options = stoutwall_get_options(); add_action('admin_menu', [$this, 'addAdminMenu']); add_action('admin_init', [$this, 'registerSettings']); add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']); add_filter('plugin_action_links_' . plugin_basename(STOUTWALL_PLUGIN_DIR . 'stoutwall.php'), [$this, 'actionLinks']); add_action('admin_notices', [$this, 'legacyPluginNotice']); add_action('admin_post_stoutwall_export', [$this, 'handleExport']); add_action('admin_post_stoutwall_import', [$this, 'handleImport']); add_action('admin_post_stoutwall_unlock', [$this, 'handleUnlock']); add_action('admin_post_stoutwall_allowlist', [$this, 'handleAllowlist']); } /** * Avisa quando a versão antiga (ProtectMyWordPress, distribuída no GitHub) ainda está ativa. * * @return void */ public function legacyPluginNotice() { if (!current_user_can('activate_plugins') || !function_exists('is_plugin_active') || !is_plugin_active('protectmywordpress/protectmywordpress.php')) { return; } printf( '

%s

', esc_html__('Stoutwall replaces ProtectMyWordPress and has already imported its settings. Please deactivate and delete ProtectMyWordPress.', 'stoutwall') ); } /** * Adiciona a página de configurações do plugin ao menu do WordPress. * * @return void */ public function addAdminMenu() { $this->hook_suffix = add_options_page( __('Stoutwall Settings', 'stoutwall'), 'Stoutwall', 'manage_options', 'stoutwall', [$this, 'settingsPage'] ); } /** * Link "Settings" na lista de plugins. * * @param array $links Links existentes. * @return array */ public function actionLinks($links) { array_unshift($links, sprintf( '%s', esc_url(admin_url('options-general.php?page=stoutwall')), esc_html__('Settings', 'stoutwall') )); return $links; } /** * Registra as configurações do plugin com sanitização. * * @return void */ public function registerSettings() { register_setting('stoutwall_option_group', 'stoutwall_options', [ 'type' => 'array', 'sanitize_callback' => [$this, 'sanitizeOptions'], ]); } /** * Sanitiza as opções antes de salvar no banco de dados. * * @param array $input Valores recebidos do formulário. * @return array Valores sanitizados. */ public function sanitizeOptions($input) { return stoutwall_sanitize_options($input); } /** * Configurações em JSON, para exportar. * * @return string */ public static function exportJson() { return (string) wp_json_encode([ 'plugin' => 'stoutwall', 'version' => STOUTWALL_VERSION, 'options' => stoutwall_get_options(), 'allowlist' => stoutwall_get_allowlist(), ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } /** * Lê um JSON exportado e devolve as opções sanitizadas. * * @param string $json Conteúdo do arquivo. * @return array|null ['options' => array, 'allowlist' => array|null], ou null se o arquivo não for do Stoutwall. */ public static function parseImport($json) { $data = json_decode($json, true); if (!is_array($data) || !isset($data['plugin'], $data['options']) || 'stoutwall' !== $data['plugin'] || !is_array($data['options'])) { return null; } return [ // Chaves que não vieram no arquivo (de versões mais antigas) mantêm o padrão. 'options' => stoutwall_sanitize_options(wp_parse_args($data['options'], stoutwall_default_options())), 'allowlist' => isset($data['allowlist']) && is_array($data['allowlist']) ? $data['allowlist'] : null, ]; } /** * Aplica um import já validado. * * @param array $import Resultado de parseImport(). * @return void */ public static function applyImport($import) { update_option('stoutwall_options', $import['options']); if (null !== $import['allowlist']) { stoutwall_save_allowlist($import['allowlist']); } } /** * URL de uma aba da página. * * @param string $tab Aba. * @param array $args Parâmetros extras. * @return string */ private static function tabUrl($tab, $args = []) { return add_query_arg(array_merge(['page' => 'stoutwall', 'tab' => $tab], $args), admin_url('options-general.php')); } /** * Baixa as configurações em JSON. * * @return void */ public function handleExport() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Access denied.', 'stoutwall'), 403); } check_admin_referer('stoutwall_export'); nocache_headers(); header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename="stoutwall-settings-' . gmdate('Y-m-d') . '.json"'); echo self::exportJson(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON para download. exit; } /** * Importa configurações de um arquivo JSON enviado. * * @return void */ public function handleImport() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Access denied.', 'stoutwall'), 403); } check_admin_referer('stoutwall_import'); $tmp = isset($_FILES['stoutwall_file']['tmp_name']) ? sanitize_text_field(wp_unslash($_FILES['stoutwall_file']['tmp_name'])) : ''; $import = null; if ('' !== $tmp && is_uploaded_file($tmp)) { $import = self::parseImport((string) file_get_contents($tmp)); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents } if (null === $import) { wp_safe_redirect(self::tabUrl('tools', ['stoutwall_notice' => 'import_failed'])); exit; } self::applyImport($import); wp_safe_redirect(self::tabUrl('tools', ['stoutwall_notice' => 'imported'])); exit; } /** * Salva a lista de IPs permitidos (ou adiciona o IP atual). * * @return void */ public function handleAllowlist() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Access denied.', 'stoutwall'), 403); } check_admin_referer('stoutwall_allowlist'); if (isset($_POST['add_current'])) { $entries = stoutwall_get_allowlist(); $entries[] = stoutwall_client_ip(); } else { $raw = isset($_POST['allowlist']) ? sanitize_textarea_field(wp_unslash($_POST['allowlist'])) : ''; $entries = preg_split('/[\s,]+/', $raw); } $invalid = stoutwall_save_allowlist($entries); wp_safe_redirect(self::tabUrl('tools', $invalid ? ['stoutwall_notice' => 'allowlist_invalid', 'stoutwall_invalid' => rawurlencode(implode(' ', array_slice($invalid, 0, 5)))] : ['stoutwall_notice' => 'allowlist_saved'])); exit; } /** * Remove bloqueios de login. * * @return void */ public function handleUnlock() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Access denied.', 'stoutwall'), 403); } check_admin_referer('stoutwall_unlock'); $id = isset($_POST['lockout']) ? sanitize_text_field(wp_unslash($_POST['lockout'])) : ''; Stoutwall_Login_Limiter::unlock('' === $id ? null : $id); wp_safe_redirect(self::tabUrl('tools', ['stoutwall_notice' => 'unlocked'])); exit; } /** * Enfileira os arquivos CSS e JS apenas na página do plugin. * * @param string $hook_suffix O sufixo do hook da página atual. * @return void */ public function enqueueAssets($hook_suffix) { if ($hook_suffix !== $this->hook_suffix) { return; } wp_enqueue_style( 'stoutwall-admin', STOUTWALL_PLUGIN_URL . 'assets/css/admin-style.css', [], $this->assetVersion('assets/css/admin-style.css') ); wp_enqueue_script( 'stoutwall-admin', STOUTWALL_PLUGIN_URL . 'assets/js/admin-script.js', ['jquery'], $this->assetVersion('assets/js/admin-script.js'), true ); wp_localize_script('stoutwall-admin', 'stoutwallAdmin', [ 'ajaxUrl' => wp_make_link_relative(admin_url('admin-ajax.php')), 'nonce' => wp_create_nonce('stoutwall_check_status'), 'auditNonce' => wp_create_nonce('stoutwall_run_audit'), 'i18n' => [ 'copied' => __('Copied.', 'stoutwall'), 'copyFailed' => __('Select the text and copy it manually.', 'stoutwall'), 'auditRunning' => __('Running the audit…', 'stoutwall'), 'auditFailed' => __('Could not run the audit.', 'stoutwall'), 'good' => __('OK', 'stoutwall'), 'recommended' => __('Recommended', 'stoutwall'), 'critical' => __('Critical', 'stoutwall'), 'skipped' => __('Not tested', 'stoutwall'), 'checkFailed' => __('Could not check the module status.', 'stoutwall'), 'requestError' => __('Could not reach the server.', 'stoutwall'), 'checking' => __('Checking…', 'stoutwall'), 'unsaved' => __('You have unsaved changes.', 'stoutwall'), 'ok' => __('Working', 'stoutwall'), 'off' => __('Off', 'stoutwall'), 'error' => __('Problem', 'stoutwall'), /* translators: 1: number of protections working, 2: number of protections with a problem. */ 'summary' => __('%1$s working, %2$s with a problem.', 'stoutwall'), /* translators: %s: number of protections working. */ 'summaryOk' => __('All %s active protections are working.', 'stoutwall'), ], ]); } /** * Versão do asset com a data de modificação do arquivo, para que caches (LiteSpeed, CDN, * navegador) não sirvam um CSS/JS antigo quando o arquivo muda sem troca de versão. * * @param string $path Caminho relativo à pasta do plugin. * @return string */ private function assetVersion($path) { $mtime = @filemtime(STOUTWALL_PLUGIN_DIR . $path); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged return $mtime ? STOUTWALL_VERSION . '.' . $mtime : STOUTWALL_VERSION; } /** * Retorna os dados (título e descrição) para cada opção. * * @return array */ private function getOptionData() { return [ 'disable_registration' => [ 'title' => __('Disable registration', 'stoutwall'), 'description' => __('Prevents new user registrations.', 'stoutwall'), ], 'disable_file_editors' => [ 'title' => __('Disable file editors', 'stoutwall'), 'description' => __('Disables theme and plugin file editing in the dashboard.', 'stoutwall'), ], 'prevent_uploads_code_execution' => [ 'title' => __('Block PHP in uploads', 'stoutwall'), 'description' => __('Adds .htaccess rules that block PHP files in the uploads folder (Apache and LiteSpeed).', 'stoutwall'), ], 'hide_wp_version' => [ 'title' => __('Hide version number', 'stoutwall'), 'description' => __('Removes the WordPress version from the page head and feeds.', 'stoutwall'), ], 'prevent_login_feedback' => [ 'title' => __('Generic login errors', 'stoutwall'), 'description' => __('Shows a generic message when a login fails, without saying whether the username exists.', 'stoutwall'), ], 'disable_directory_browsing' => [ 'title' => __('Disable directory listing', 'stoutwall'), 'description' => __('Adds an empty index.php to the uploads folder so its files cannot be listed.', 'stoutwall'), ], 'disable_user_enumeration' => [ 'title' => __('Prevent user enumeration', 'stoutwall'), 'description' => __('Hides usernames from ?author=N, the REST API, the users sitemap and oEmbed.', 'stoutwall'), ], 'unset_x_powered_by' => [ 'title' => __('Remove X-Powered-By header', 'stoutwall'), 'description' => __('Removes the header that reveals the PHP version.', 'stoutwall'), ], 'block_admin_username' => [ 'title' => __('Block the "admin" username', 'stoutwall'), 'description' => __('Blocks new registrations that use the username "admin".', 'stoutwall'), ], 'disable_xmlrpc' => [ 'title' => __('Disable XML-RPC', 'stoutwall'), 'description' => __('Turns off XML-RPC, including pingbacks, and removes the X-Pingback header.', 'stoutwall'), ], 'block_registration_same_login_display' => [ 'title' => __('Require a display name other than the username', 'stoutwall'), 'description' => __('Prevents users from saving a profile whose public display name is their username.', 'stoutwall'), ], 'limit_login_attempts' => [ 'title' => __('Limit login attempts', 'stoutwall'), 'description' => __('After 5 failed logins in 15 minutes, the IP address is locked out for 15 minutes. Repeated lockouts last longer, up to 24 hours.', 'stoutwall'), ], 'security_headers' => [ 'title' => __('Security headers', 'stoutwall'), 'description' => __('Sends X-Content-Type-Options, X-Frame-Options (same site only) and Referrer-Policy. Turn off if another site needs to show yours in a frame.', 'stoutwall'), ], 'hsts' => [ 'title' => __('Force HTTPS in browsers (HSTS)', 'stoutwall'), 'description' => __('Tells browsers to always use HTTPS on this site for a year. Only turn on when HTTPS works on every page: browsers remember it even if you turn it off.', 'stoutwall'), ], 'disable_application_passwords' => [ 'title' => __('Disable application passwords', 'stoutwall'), 'description' => __('Turns off the passwords that apps and services use to access the REST API. Keep them if you connect the mobile app, Zapier or similar tools.', 'stoutwall'), ], 'restrict_rest_api' => [ 'title' => __('Require login for the REST API', 'stoutwall'), 'description' => __('Visitors who are not logged in cannot use the REST API, except oEmbed, Contact Form 7, WooCommerce checkout and Jetpack. Can break plugins or themes that load content through the API.', 'stoutwall'), ], ]; } /** * Agrupa as opções por tipo de proteção, na ordem exibida na página. * * @return array */ private function getGroups() { return [ 'accounts' => [ 'title' => __('Accounts and login', 'stoutwall'), 'description' => __('Make it harder to guess usernames or create accounts on this site.', 'stoutwall'), 'options' => ['limit_login_attempts', 'disable_registration', 'block_admin_username', 'block_registration_same_login_display', 'prevent_login_feedback', 'disable_user_enumeration'], ], 'files' => [ 'title' => __('Files and uploads', 'stoutwall'), 'description' => __('Stop uploaded or edited files from being used to run code.', 'stoutwall'), 'options' => ['disable_file_editors', 'prevent_uploads_code_execution', 'disable_directory_browsing'], ], 'disclosure' => [ 'title' => __('Server details', 'stoutwall'), 'description' => __('Hide the software versions that attackers use to pick known exploits.', 'stoutwall'), 'options' => ['hide_wp_version', 'unset_x_powered_by'], ], 'browser' => [ 'title' => __('Browser protection', 'stoutwall'), 'description' => __('Tell browsers to block common attacks such as clickjacking and file type confusion.', 'stoutwall'), 'options' => ['security_headers', 'hsts'], ], 'remote' => [ 'title' => __('Remote access', 'stoutwall'), 'description' => __('Close the interfaces that apps and scripts use to reach the site from outside, when you do not need them.', 'stoutwall'), 'options' => ['disable_xmlrpc', 'disable_application_passwords', 'restrict_rest_api'], ], ]; } /** * Abas da página. * * @return array */ private function getTabs() { return [ 'protections' => __('Protections', 'stoutwall'), 'server' => __('Server rules', 'stoutwall'), 'audit' => __('Audit', 'stoutwall'), 'tools' => __('Tools', 'stoutwall'), ]; } /** * Renderiza a página de configurações do plugin. * * @return void */ public function settingsPage() { $tabs = $this->getTabs(); $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'protections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- só escolhe a aba. $tab = isset($tabs[$tab]) ? $tab : 'protections'; ?>
renderHeader(); ?> renderServerTab(); break; case 'audit': $this->renderAuditTab(); break; case 'tools': $this->renderToolsTab(); break; default: $this->renderProtectionsTab(); } ?>
getOptionData(); $total = count($option_data); $enabled = 0; foreach (array_keys($option_data) as $key) { $enabled += empty($this->options[$key]) ? 0 : 1; } ?>

' . esc_html($enabled) . '', esc_html($total) ); ?>

getOptionData(); ?>
getGroups() as $group_key => $group) : ?>

  • renderIpSource(); } ?>
    options[$key] ?? 0); ?>>

__('Direct connection (REMOTE_ADDR) – recommended', 'stoutwall'), 'cf_connecting_ip' => __('Cloudflare (CF-Connecting-IP)', 'stoutwall'), 'x_real_ip' => __('Proxy header X-Real-IP', 'stoutwall'), 'x_forwarded_for' => __('Proxy header X-Forwarded-For', 'stoutwall'), ]; $current = $this->options['login_ip_source']; ?>
>

' . esc_html(stoutwall_client_ip() ? stoutwall_client_ip() : '?') . '' ); ?>

['success', __('Settings imported.', 'stoutwall')], 'import_failed' => ['error', __('This file is not a Stoutwall settings export.', 'stoutwall')], 'unlocked' => ['success', __('Lockouts removed.', 'stoutwall')], 'allowlist_saved' => ['success', __('Allowed IP addresses saved.', 'stoutwall')], /* translators: %s: invalid entries. */ 'allowlist_invalid' => ['warning', sprintf(__('Saved, but these entries are not valid IP addresses or ranges and were skipped: %s', 'stoutwall'), $invalid)], ]; $lockouts = Stoutwall_Login_Limiter::activeLockouts(); $allowlist = stoutwall_get_allowlist(); $my_ip = stoutwall_client_ip(); $post_url = admin_url('admin-post.php'); if (isset($notices[$notice])) { printf('

%s

', esc_attr($notices[$notice][0]), esc_html($notices[$notice][1])); } ?>

$entry) : ?>

". Run "wp help stoutwall" for every command.', 'stoutwall'); ?>