array( 'class' => true ), 'div' => array( 'class' => true, 'data-id' => true, 'data-lang' => true, 'data-rule-id' => true, 'data-keyword' => true, 'data-synonyms' => true, 'style' => true, 'title' => true ), 'span' => array( 'class' => true, 'title' => true, 'style' => true ), 'a' => array( 'href' => true, 'class' => true, 'title' => true, 'target' => true, 'rel' => true, 'data-id' => true ), 'button' => array( 'class' => true, 'title' => true, 'type' => true, 'data-id' => true, 'data-pilar-id' => true, 'data-rule-id' => true, 'data-keyword' => true ), 'label' => array( 'class' => true ), 'input' => array( 'type' => true, 'class' => true, 'value' => true, 'checked' => true, 'data-id' => true, 'placeholder' => true ), 'svg' => array( 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'class' => true, 'xmlns' => true, 'aria-hidden' => true ), 'path' => array( 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true ), 'polygon' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true ), 'polyline' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true ), 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true ), 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true ), ); } public function init() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); // Onboarding glue add_action( 'wp_ajax_flink_hide_onboarding', array( $this, 'ajax_hide_onboarding' ) ); add_action( 'admin_init', array( $this, 'maybe_redirect_to_onboarding' ) ); // Settings actions add_action( 'admin_init', array( $this, 'maybe_delete_openai_key' ) ); // Pin sidebar highlight to "Enlazado" when the user is on a // legacy URL that we couldn't safely redirect (eg. one with // edit params). WordPress decides which submenu item gets the // active class based on $submenu_file; we override it so the // sidebar always shows the user where they are. add_filter( 'submenu_file', array( $this, 'pin_enlazado_submenu_highlight' ) ); } /** * Force the sidebar's "Enlazado" entry to look active when the * user is rendering one of the legacy hidden pages. Without this, * pages like ?page=flinkil-pilares&edit=12 render fine but the * sidebar shows no active item, which feels broken. */ public function pin_enlazado_submenu_highlight( $submenu_file ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $legacy = array( 'flinkil-pilares', 'flinkil-reglas', 'flinkil-exitos', ); if ( in_array( $page, $legacy, true ) ) { return 'flinkil-enlazado'; } // System legacy pages keep the "Sistema" hub (flinkil-ajustes) // highlighted. Herramientas is common; KB is Paid-only. $sistema_legacy = array( 'flinkil-herramientas' ); if ( in_array( $page, $sistema_legacy, true ) ) { return 'flinkil-ajustes'; } return $submenu_file; } /** * Handles the "Eliminar clave" button in Ajustes → IA. * Validates nonce + capability, then wipes the option. * Redirects back to the same tab with a success flag so the * user gets a clear visual confirmation. */ public function maybe_delete_openai_key() { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( empty( $_POST['flink_delete_openai_key'] ) ) return; if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Sin permisos suficientes.', 403 ); } check_admin_referer( 'flink_delete_openai_key', 'flink_delete_openai_key_nonce' ); delete_option( 'flink_openai_key' ); wp_safe_redirect( add_query_arg( array( 'page' => 'flinkil-ajustes', 'tab' => 'ia', 'key_deleted' => '1' ), admin_url( 'admin.php' ) ) ); exit; } /** * AJAX: hide the "Primeros pasos" item from the menu. * Stores the choice in an option; the menu registration reads * it on the next admin page load. Reversible from Ajustes if we * later add a setting for it. */ public function ajax_hide_onboarding() { if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( '', 403 ); check_ajax_referer( 'flink_hide_onboarding', 'nonce' ); update_option( 'flink_onboarding_hidden', '1' ); wp_send_json_success(); } /** * On first activation, redirect once to the onboarding page so * brand-new users land on a guided screen instead of an empty * "Crear contenido". The redirect flag is set by the activation * hook in the main plugin file and consumed (deleted) here. */ public function maybe_redirect_to_onboarding() { if ( ! get_transient( 'flink_onboarding_redirect' ) ) return; delete_transient( 'flink_onboarding_redirect' ); // Don't redirect during AJAX, cron, REST, or bulk plugin activations. if ( wp_doing_ajax() || wp_doing_cron() ) return; if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) return; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( isset( $_GET['activate-multi'] ) ) return; if ( ! current_user_can( 'manage_options' ) ) return; wp_safe_redirect( admin_url( 'admin.php?page=flinkil-onboarding' ) ); exit; } /** * Returns the menu-and-page-layout CSS that needs to apply on every * plugin admin page. Returned as a string so it can be attached to * the main 'flink-admin' style handle via wp_add_inline_style() in * enqueue_assets() — that's the WordPress.org-compliant way to ship * inline styles. */ public function get_wide_screen_css() { return ' #wpcontent { padding-left: 20px !important; } #wpbody-content .wrap { max-width: none !important; padding-right: 24px; } /* Hide the WP auto-generated duplicate first submenu item */ /* Hide the auto-generated duplicate "04 IL" first submenu item */ #toplevel_page_04-internal-linking ul li:first-child, #toplevel_page_04-internal-linking ul li a[href="admin.php?page=04-internal-linking"], /* Hide the default first submenu item that duplicates the parent */ #toplevel_page_04-internal-linking ul li a[href*="page=04-internal-linking"]:not([href*="flinkil-"]) { display: none !important; } /* ── Simple page headers (Ajustes & Herramientas) ── These two pages dropped the illustrated header that the rest of the plugin uses, because they are functional/ technical contexts where the illustration didn\'t fit. Same typographic scale as FLINK_Page_Header (.ilph-title) so the page hierarchy stays consistent across the plugin. */ .il-page .ilset-simple-hd, .il-page .ilh-simple-hd { margin: 0 0 22px; } .il-page .ilset-simple-hd h1, .il-page .ilh-simple-hd h1 { font-size: 28px !important; font-weight: 700 !important; letter-spacing: -.03em !important; line-height: 1.1 !important; margin: 0 0 6px !important; color: var(--ds-ink, #0f1419) !important; } .il-page .ilset-simple-hd p, .il-page .ilh-simple-hd p { font-size: 15px !important; color: var(--ds-ink3, #6b7280) !important; margin: 0 !important; line-height: 1.5 !important; } '; } public function register_menu() { /* * 04 IL * ├─ Crear contenido * ├─ Crear imágenes * ├─ Traducciones * ├─ Automatizaciones * ├─ ─── ENLAZADO ─── * │ ├─ Páginas pilar * │ ├─ Reglas de enlazado * │ ├─ Enlazado semántico * │ └─ Mapa de enlaces * ├─ ─── SISTEMA ─── * │ ├─ Ajustes * │ └─ Herramientas */ add_menu_page( '4Linking', '4Linking', 'manage_options', '04-internal-linking', '__return_false', 'dashicons-networking', 30 ); // ── Primeros pasos (onboarding) ────────────────────────── // Shown until the user dismisses it via the page itself // (sets il_onboarding_hidden=1). Until then it's the first // item in the submenu — visible to first-time users. if ( get_option( 'flink_onboarding_hidden', '0' ) !== '1' ) { add_submenu_page( '04-internal-linking', 'Primeros pasos', 'Primeros pasos', 'manage_options', 'flinkil-onboarding', array( $this, '_render_onboarding' ) ); } else { // Keep it reachable by URL (so the user can re-enable from // a settings link if they ever want to), but hidden from menu. add_submenu_page( null, 'Primeros pasos', 'Primeros pasos', 'manage_options', 'flinkil-onboarding', array( $this, '_render_onboarding' ) ); } // ── ENLAZADO ───────────────────────────────────────────── // Renders a tabbed interface for the four enlazado views // (pilares / reglas / semantic / mapa). The original sub-pages // are still registered below as hidden pages so deep links // (?page=flinkil-pilares) keep working — critical because URLs // are referenced from emails, error messages and from JS inside // the plugin itself. add_submenu_page( '04-internal-linking', 'Enlazado', 'Enlazado', 'manage_options', 'flinkil-enlazado', array( $this, '_render_enlazado_hub' ) ); // ── SISTEMA ────────────────────────────────────────────── // Single hub entry with tabs: Bases de conocimiento (PRO), // Ajustes and Herramientas. Reuses the flinkil-ajustes slug as // base so its asset loading keeps working. The hub uses `sec` // (not `tab`) as its query param to avoid colliding with the // internal tabs that the Ajustes page itself uses (?tab=ia, // ?tab=enlazado, ...). add_submenu_page( '04-internal-linking', 'Sistema', 'Sistema', 'manage_options', 'flinkil-ajustes', array( $this, '_render_sistema_hub' ) ); // KB, Ajustes and Herramientas keep their own slugs as hidden // routes so deep-links keep resolving. Ajustes shares its slug // with the hub, so it isn't registered separately here. add_submenu_page( null, 'Herramientas', 'Herramientas', 'manage_options', 'flinkil-herramientas', array( $this, 'render_herramientas' ) ); // ── Hidden / direct-URL-only pages ──────────────────────── // The 4 enlazado views are registered as hidden so the existing // URLs (?page=flinkil-pilares etc.) continue to resolve. They're // not shown in the sidebar — the user reaches them via the // tabs inside the "Enlazado" hub. Direct URLs from emails, // error messages and JS code still work because the parent is // null (= no submenu placement, just route registration). add_submenu_page( null, 'Páginas pilar', 'Páginas pilar', 'manage_options', 'flinkil-pilares', array( $this, 'render_pilares' ) ); add_submenu_page( null, 'Reglas de enlazado','Reglas de enlazado','manage_options', 'flinkil-reglas', array( $this, 'render_reglas' ) ); add_submenu_page( null, 'Mapa de enlaces', 'Mapa de enlaces', 'manage_options', 'flinkil-exitos', array( $this, '_render_exitos' ) ); add_submenu_page( null, 'URLs excluidas', 'URLs excluidas', 'manage_options', 'flinkil-exclusiones', array( $this, 'render_exclusiones' ) ); // Redirect bare top-level to the right landing page. // In the full edition the landing page is "Crear contenido" — the // content-generation flow that most users open first. The free // edition doesn't include that module, so it lands on the // Enlazado hub instead, which is its main feature. add_action( 'admin_init', function () { global $pagenow; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( $pagenow !== 'admin.php' || empty( $_GET['page'] ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $page = sanitize_key( wp_unslash( $_GET['page'] ) ); if ( $page === '04-internal-linking' ) { // Full edition lands on Crear contenido; the free edition // (which doesn't ship that module) lands on Enlazado. if ( ! headers_sent() ) wp_safe_redirect( admin_url( 'admin.php?page=flinkil-enlazado' ) ); exit; } // Legacy URL → hub tab redirects. // The four enlazado views are still registered as hidden // pages (so direct loads work and any in-flight bookmark // doesn't 404), but visiting them via the menu / a pasted // link should land on the hub with the matching tab so the // sidebar highlight stays coherent. Only redirect plain // URLs without a query that already names a tab — this // keeps internal callers that pass extra params (eg. // ?page=flinkil-pilares&edit=12) free to opt out by also // passing &skip_hub=1. $legacy_to_tab = array( 'flinkil-pilares' => 'pilares', 'flinkil-reglas' => 'reglas', 'flinkil-exitos' => 'mapa', ); // System pages → Sistema hub sections (uses `sec` param). // Herramientas is common to both editions; KB is Paid-only. $legacy_to_sistema_sec = array( 'flinkil-herramientas' => 'herramientas', ); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( isset( $legacy_to_sistema_sec[ $page ] ) && empty( $_GET['skip_hub'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $extras = array_diff_key( $_GET, array( 'page' => 1 ) ); if ( empty( $extras ) ) { wp_safe_redirect( admin_url( 'admin.php?page=flinkil-ajustes&sec=' . $legacy_to_sistema_sec[ $page ] ) ); exit; } } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( isset( $legacy_to_tab[ $page ] ) && empty( $_GET['skip_hub'] ) ) { // Only redirect if the URL is "naked" — no extra params // beyond `page`. This protects deep links like edit // dialogs from being broken by the redirect. // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $extras = array_diff_key( $_GET, array( 'page' => 1 ) ); if ( empty( $extras ) ) { wp_safe_redirect( admin_url( 'admin.php?page=flinkil-enlazado&tab=' . $legacy_to_tab[ $page ] ) ); exit; } } } ); } /** * Hub renderer for the unified "Enlazado" page. Delegates to one * of the four sub-renderers based on the active tab. Each tab keeps * its own URL slug as a query param so deep-linking within the hub * is possible (?page=flinkil-enlazado&tab=reglas). * * The tab strip itself is rendered inline here (not by each * sub-renderer) so we don't touch the four existing render_* * methods. They keep behaving exactly as before — header, body, * everything. We just wrap them in a tab navigation. */ public function _render_enlazado_hub() { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'pilares'; $valid = array( 'pilares', 'reglas', 'mapa', ); if ( ! in_array( $tab, $valid, true ) ) $tab = 'pilares'; // Each tab carries a small line icon (18×18) for the compact // card-style navigation. Icons follow the Lucide-style stroke // look defined in the brand manual. $tabs = array( 'pilares' => array( 'label' => 'Páginas pilar', 'method' => 'render_pilares', 'icon' => '', ), 'reglas' => array( 'label' => 'Reglas', 'method' => 'render_reglas', 'icon' => '', ), 'mapa' => array( 'label' => 'Mapa de enlaces', 'method' => '_render_exitos', 'icon' => '', ), ); // Stash the navigation so the active tab's FLINK_Page_Header // call renders it directly below the section header (option 2: // navigation integrated into the content, not floating on top). self::$pending_hubnav = self::render_hub_nav( 'flinkil-enlazado', $tab, $tabs ); // Wrap in .il-screen so every Enlazado tab shares the same // page background and header position as Crear contenido. echo '
'; // Delegate to the original renderer for the active tab. $method = $tabs[ $tab ]['method']; if ( method_exists( $this, $method ) ) { $this->$method(); } echo '
'; // Clear it afterwards so it never leaks into an unrelated page. self::$pending_hubnav = ''; } /** * Hub renderer for "Sistema". Tabs: Bases de conocimiento (PRO), * Ajustes, Herramientas. Reuses the flinkil-ajustes slug as base and * uses `sec` as its query param (NOT `tab`) so it never collides with * the internal tabs the Ajustes page uses (?tab=ia, ?tab=enlazado...). */ public function _render_sistema_hub() { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $sec = isset( $_GET['sec'] ) ? sanitize_key( wp_unslash( $_GET['sec'] ) ) : 'ajustes'; $tabs = array(); $tabs['ajustes'] = array( 'label' => 'Ajustes', 'method' => 'render_ajustes', 'header_key' => 'ajustes', 'title' => 'Ajustes', 'subtitle' => 'Configuración global del plugin. Los cambios se aplican inmediatamente.', 'icon' => '', ); $tabs['herramientas'] = array( 'label' => 'Herramientas', 'method' => 'render_herramientas', 'header_key' => 'herramientas', 'title' => 'Herramientas', 'subtitle' => 'Mantenimiento, copias de seguridad y monitorización del plugin.', 'icon' => '', ); if ( ! isset( $tabs[ $sec ] ) ) { // Requested section doesn't exist in this edition → fall back // to Ajustes, which is always present (Free and Paid). $sec = isset( $tabs['ajustes'] ) ? 'ajustes' : array_key_first( $tabs ); } // Wrap in .il-screen for the shared page background, matching the // Enlazado and Crear contenido hubs. echo '
'; // Stash the hub navigation so FLINK_Page_Header::render() paints it // right below the section header — same pattern as the other hubs. self::$pending_hubnav = self::render_hub_nav( 'flinkil-ajustes', $sec, $tabs, 'sec' ); // Ajustes and Herramientas don't call Page_Header themselves, so the // hub paints their header here (icon + title + subtitle) for the same // look as Crear contenido. KB has header_key empty because it renders // its own header (and thus the navigation) on its own. if ( ! empty( $tabs[ $sec ]['header_key'] ) ) { FLINK_Page_Header::render( $tabs[ $sec ]['header_key'], $tabs[ $sec ]['title'], $tabs[ $sec ]['subtitle'], '' ); } $method = $tabs[ $sec ]['method']; if ( method_exists( $this, $method ) ) { $this->$method(); } echo '
'; self::$pending_hubnav = ''; } /** * Render the compact card-style tab navigation shared by every hub * (Enlazado, and later Crear contenido / Sistema). Centralised so the * three hubs stay visually identical and a style tweak applies once. * * @param string $page The admin page slug (e.g. 'flinkil-enlazado'). * @param string $active The active tab key. * @param array $tabs Map of key => [ 'label' => ..., 'icon' => ... ]. */ /** * HTML of the hub navigation for the current request, stashed by the * hub renderer so that FLINK_Page_Header::render() can output it right * below the section header. This places the navigation "inside" the * content (after the title) rather than floating above everything. * * @var string */ public static $pending_hubnav = ''; public static function render_hub_nav( $page, $active, $tabs, $param = 'tab' ) { // SVG icons are emitted via wp_kses with an explicit allow-list // (the same one Page_Header uses) so the stroke icons survive // sanitisation instead of being stripped by wp_kses_post. $svg_allowed = array( 'svg' => array( 'viewbox' => true, 'xmlns' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true ), 'path' => array( 'd' => true, 'fill' => true ), 'polygon' => array( 'points' => true ), 'polyline' => array( 'points' => true ), 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true ), 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true ), ); $html = ''; return $html; } /** Proxy render functions — load module and call its render_page() */ public function _render_onboarding() { // Page-specific CSS and JS — enqueued here rather than from // enqueue_assets() because they're only needed on this page. wp_enqueue_style( 'il-page-onboarding', FLINK_PLUGIN_URL . 'admin/css/page-onboarding.css', array( 'flink-admin' ), filemtime( FLINK_PLUGIN_DIR . 'admin/css/page-onboarding.css' ) ); wp_enqueue_script( 'il-page-onboarding', FLINK_PLUGIN_URL . 'admin/js/page-onboarding.js', array( 'jquery' ), filemtime( FLINK_PLUGIN_DIR . 'admin/js/page-onboarding.js' ), true ); // Pass nonce + redirect URL via wp_localize_script — the // WordPress.org-compliant way to ship PHP values to a JS file. // PRO lands on Crear contenido (the hub); Free has no such page, // so it lands on the Enlazado hub instead. $onboarding_redirect = admin_url( 'admin.php?page=flinkil-enlazado' ); wp_localize_script( 'il-page-onboarding', 'FLINK_ONBOARDING', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'flink_hide_onboarding' ), 'redirectUrl' => $onboarding_redirect, ) ); // The view renders directly — no module needed echo '
'; require FLINK_PLUGIN_DIR . 'views/page-onboarding.php'; echo '
'; } public function _render_exitos() { FLINK_Link_Stats::maybe_install(); wp_enqueue_style( 'il-page-exitos', FLINK_PLUGIN_URL . 'admin/css/page-exitos.css', array( 'flink-admin' ), filemtime( FLINK_PLUGIN_DIR . 'admin/css/page-exitos.css' ) ); wp_enqueue_script( 'flink-exitos', FLINK_PLUGIN_URL . 'admin/js/exitos.js', array( 'jquery' ), filemtime( FLINK_PLUGIN_DIR . 'admin/js/exitos.js' ), true ); wp_localize_script( 'flink-exitos', 'FLINK_EXITOS', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'flink_nonce' ), 'semantic' => class_exists( 'FLINK_Semantic_DB' ) ? 1 : 0, ) ); require FLINK_PLUGIN_DIR . 'includes/views/page-exitos.php'; } private function _get_module( $id ) { global $flink_modules; return isset( $flink_modules[ $id ] ) ? $flink_modules[ $id ] : null; } public function enqueue_assets( $hook ) { // Match any subpage of this plugin regardless of exact hook format WordPress uses $is_plugin_page = ( $hook === 'toplevel_page_04-internal-linking' ) || ( strpos( $hook, 'flinkil-' ) !== false ) || ( strpos( $hook, '04-internal-linking' ) !== false ) || ( strpos( $hook, 'flinkil-pilares' ) !== false ); if ( ! $is_plugin_page ) return; wp_enqueue_style( 'flink-google-fonts', 'https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500;600&display=swap', array(), FLINK_VERSION ); wp_enqueue_style( 'flink-admin', FLINK_PLUGIN_URL . 'admin/css/admin.css', array( 'flink-google-fonts' ), filemtime( FLINK_PLUGIN_DIR . 'admin/css/admin.css' ) ); // Attach the wide-screen + menu CSS as inline style on the same handle. // get_wide_screen_css() returns a hard-coded CSS string with no user // input; wp_add_inline_style() is the WordPress API for injecting // inline CSS and handles output safely (it is not echoed directly). wp_add_inline_style( 'flink-admin', $this->get_wide_screen_css() ); // Inject the shared illustrated-header styles too — these used to // be emitted from a separate admin_head hook. get_styles() likewise // returns a static, hard-coded CSS string. if ( class_exists( 'FLINK_Page_Header' ) ) { wp_add_inline_style( 'flink-admin', FLINK_Page_Header::get_styles() ); } wp_enqueue_script( 'flink-admin', FLINK_PLUGIN_URL . 'admin/js/admin.js', array( 'jquery' ), FLINK_VERSION . '.' . filemtime( FLINK_PLUGIN_DIR . 'admin/js/admin.js' ), true ); // Settings crawl modal — only on the Ajustes page (it depends on // the modal HTML rendered there). if ( strpos( $hook, 'flinkil-ajustes' ) !== false ) { wp_enqueue_script( 'flink-settings-crawl', FLINK_PLUGIN_URL . 'admin/js/settings-crawl.js', array( 'jquery' ), filemtime( FLINK_PLUGIN_DIR . 'admin/js/settings-crawl.js' ), true ); wp_enqueue_script( 'flink-settings-tipos', FLINK_PLUGIN_URL . 'admin/js/settings-tipos.js', array( 'jquery' ), filemtime( FLINK_PLUGIN_DIR . 'admin/js/settings-tipos.js' ), true ); } // Get active languages from Polylang (if available) for the rule form $active_langs = array(); if ( class_exists( 'FLINK_Polylang_Bridge' ) ) { $active_langs = FLINK_Polylang_Bridge::get_active_languages(); } elseif ( function_exists( 'pll_languages_list' ) ) { $slugs = pll_languages_list( array( 'fields' => 'slug' ) ); $names = pll_languages_list( array( 'fields' => 'name' ) ); if ( is_array( $slugs ) ) { foreach ( $slugs as $i => $slug ) { $active_langs[] = array( 'slug' => $slug, 'name' => ( is_array( $names ) && isset( $names[ $i ] ) ) ? $names[ $i ] : strtoupper( $slug ), ); } } } // Detect site default language from Polylang $default_lang = ''; if ( function_exists( 'pll_default_language' ) ) { $default_lang = pll_default_language(); } elseif ( ! empty( $active_langs ) ) { $default_lang = $active_langs[0]['slug']; } wp_localize_script( 'flink-admin', 'FLINK_DATA', array( 'rulesUrl' => admin_url( 'admin.php?page=flinkil-reglas' ), 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'flink_nonce' ), // Only a boolean flag is exposed — never the key itself. // The key stays server-side and is consumed only by AJAX // handlers that read it from get_option(). Exposing the // raw key to JS would leak it into the page DOM, browser // extensions and any screenshots of devtools. 'has_openai' => ! empty( get_option( 'flink_openai_key', '' ) ), 'active_langs' => $active_langs, 'default_lang' => $default_lang, 'needs_processing' => 0, // ── Legal URLs for the OpenAI BYOK consent modal (v4.5.0) ── // Auto-selects the English variants when WordPress is // running in any English locale (en_US, en_GB, etc.) so // the modal links to documents the admin can actually // read. Can still be overridden via `il_legal_urls` // filter for non-standard deployments. 'legalUrls' => apply_filters( 'flink_legal_urls', ( 0 === strpos( get_locale(), 'en' ) ) ? array( 'privacy' => 'https://4linking.com/en/privacy-policy/', 'terms' => 'https://4linking.com/en/terms-and-conditions/', ) : array( 'privacy' => 'https://4linking.com/politica-de-privacidad/', 'terms' => 'https://4linking.com/terminos-y-condiciones/', ) ), ) ); } /* ══════════════════════════════════════════════════════════ PÁGINAS PILAR ══════════════════════════════════════════════════════════ */ public function render_pilares() { $pilares_obj = new FLINK_Pilares(); $all_pilares = $pilares_obj->get_all(); $links_map = array(); $total_links = 0; foreach ( $all_pilares as $p ) { $cnt = FLINK_Stats::count_real_links_for_pilar( $p ); $links_map[ $p->id ] = $cnt; $total_links += $cnt; } $total_pilares = count( $all_pilares ); $active_langs = array(); if ( class_exists( 'FLINK_Polylang_Bridge' ) ) { $active_langs = FLINK_Polylang_Bridge::get_active_languages(); } $default_lang = function_exists('pll_default_language') ? pll_default_language() : ( $active_langs[0]['slug'] ?? '' ); // Count per language for chips $per_lang = array(); foreach ( $all_pilares as $p ) { $lg = $p->idioma ?: ''; $per_lang[ $lg ] = ( $per_lang[ $lg ] ?? 0 ) + 1; } ?>
' . '' . 'Nueva página pilar' . ''; FLINK_Page_Header::render( 'pilares', 'Páginas pilar', 'Define las páginas clave de tu arquitectura de contenido. Las reglas de enlazado apuntarán a estas páginas.', $btn ); ?> 0 ? round( $total_links / $total_pilares, 1 ) : 0; ?>
Pilares totales
Enlaces a pilares
Idiomas activos
enlaces por pilar Media por pilar
IDIOMA

Todavía no hay páginas pilar.

Crea la primera con el botón "Nueva página pilar".

Idioma Nombre URL Enlaces
= $page_size ) break; echo wp_kses( $this->render_pilar_row( $p, $links_map[ $p->id ] ?? 0 ), self::admin_kses_allowed() ); $shown++; } ?>
100 ) : ?>
Mostrando 100 de páginas pilar.
render_pilar_row( $p, 0 ); } public function render_pilar_row( $p, $link_count = 0 ) { $idioma_upper = ! empty( $p->idioma ) ? strtoupper( $p->idioma ) : 'ALL'; ob_start(); ?>
nombre ); ?>
url ); ?>
get_all(); $reglas = $reglas_obj->get_all(); $total_reglas = count( $reglas ); $active_count = count( array_filter( $reglas, function($r){ return (bool)$r->activo; } ) ); $active_langs = array(); if ( class_exists( 'FLINK_Polylang_Bridge' ) ) { $active_langs = FLINK_Polylang_Bridge::get_active_languages(); } elseif ( function_exists( 'pll_languages_list' ) ) { $slugs = pll_languages_list( array( 'fields' => 'slug' ) ); $names = pll_languages_list( array( 'fields' => 'name' ) ); foreach ( (array) $slugs as $i => $slug ) { $active_langs[] = array( 'slug' => $slug, 'name' => $names[$i] ?? strtoupper($slug) ); } } $default_lang = function_exists('pll_default_language') ? pll_default_language() : ( $active_langs[0]['slug'] ?? '' ); // Count per lang for chips $per_lang = array(); foreach ( $reglas as $r ) { $lg = ! empty( $r->idioma ) ? $r->idioma : ( $default_lang ?: '__all__' ); $per_lang[ $lg ] = ( $per_lang[ $lg ] ?? 0 ) + 1; } $kw_set = array_unique( array_column( $reglas, 'nombre' ) ); ?>
' . '' . 'Aplicar enlaces' . '' . ''; FLINK_Page_Header::render( 'reglas', 'Reglas de enlazado', 'Cuando una palabra clave aparezca en cualquier publicación, se convertirá automáticamente en un enlace hacia la URL o página pilar que indiques.', $btn ); ?> id ); } $avg_per_rule = $total_reglas > 0 ? round( $total_rule_links / $total_reglas, 1 ) : 0; $active_pct = $total_reglas > 0 ? round( ( $active_count / $total_reglas ) * 100 ) : 0; ?>
Reglas totales
% en uso Activas
Enlaces generados
enlaces Media por regla

Todavía no hay reglas de enlazado.

Crea la primera con el botón "Nueva regla".

Idioma Keyword Sinónimos Pilar / URL Prioridad Máx Enlaces Estado
render_regla_item( $r, $pilares, $active_langs, $default_lang ), self::admin_kses_allowed() ); ?>
destino_valor ) ) { $pilar_nombre = '—'; $dest_url = $r->destino_valor; $dest_href = esc_url( $r->destino_valor ); } else { foreach ( $pilares as $p ) { if ( (int) $p->id === (int) $r->pilar_id ) { $pilar_nombre = $p->nombre; $dest_url = $p->url; $dest_href = esc_url( $p->url ); break; } } } $keyword = ! empty( $r->nombre ) ? $r->nombre : '—'; $prio = (int) $r->prioridad; $max_links = (int) ( $r->max_enlaces ?? 1 ); $active = (bool) $r->activo; $prio_cls = $prio >= 75 ? 'hi' : ( $prio >= 40 ? 'mid' : 'lo' ); // Synonyms $syn_list = array(); $syn_text = ''; if ( ! empty( $r->descripcion ) ) { $syn_list = array_filter( array_map( 'trim', explode( "\n", $r->descripcion ) ) ); $syn_text = esc_attr( strtolower( $r->descripcion ) ); } // Link count $link_count = (int) FLINK_Stats::count_links_by_rule( $r->id ); $idioma_slug = ! empty( $r->idioma ) ? $r->idioma : ( $default_lang ?: '' ); $idioma_upper = $idioma_slug ? strtoupper( $idioma_slug ) : 'ALL'; ob_start(); ?>
3 ) : ?> + sin sinónimos
—
init_for_batch(); $stripped = $linker->strip_links_for_exclusion( $exc_url ); update_option( 'flink_needs_processing', 1, false ); $msg = 'URL añadida a exclusiones' . ( $stripped > 0 ? '. Se han eliminado los enlaces salientes de ' . $stripped . ' artículo' . ( $stripped !== 1 ? 's' : '' ) : '' ) . '.'; $msg_type = 'ok'; } else { $msg_type = 'warn'; $msg = 'Esa URL ya estaba excluida.'; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. } elseif ( $_POST['flink_exc_action'] === 'remove' && ! empty( $_POST['flink_exc_url_remove'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. FLINK_Exclusiones::remove( sanitize_text_field( wp_unslash( $_POST['flink_exc_url_remove'] ) ) ); $msg_type = 'ok'; update_option( 'flink_needs_processing', 1, false ); $msg = 'URL eliminada de la lista. A partir de ahora esta página volverá a generar enlaces salientes normalmente.'; } } $exclusiones = FLINK_Exclusiones::get_all(); $count = count( $exclusiones ); ?>
0 ) { $plural = $count !== 1 ? 's' : ''; $actions = '
' . (int) $count . ' URL' . $plural . ' excluida' . $plural . '
'; } FLINK_Page_Header::render( 'exclusiones', 'URLs excluidas', 'Las páginas que añadas aquí no recibirán enlaces salientes generados por el plugin. Sí podrán seguir recibiendo enlaces entrantes desde otros artículos.', $actions ); ?>
Nota: al excluir una página, sus enlaces salientes se eliminan automáticamente. Si la reactivas, usa "Procesar posts" para regenerarlos. Los nuevos artículos sí los respetarán, pero los existentes necesitan reprocesarse.
Ir a Reglas y procesar posts
Excluir una URL

Ruta relativa /mi-pagina/ o URL completa https://...

Las páginas excluidas no insertan enlaces salientes hacia páginas pilar. Sí pueden seguir recibiendo enlaces entrantes desde otros artículos. Al excluir una URL, se eliminan los enlaces salientes que esa página ya tenía insertados.

Sin exclusiones activas

El plugin aplicará el enlazado en todos los contenidos publicados

# URL excluida
$url ) : ?>
Copias de seguridad URLs excluidas
render_exclusiones(); return; } ?>
Copia de seguridad
Exporta todo el plugin en un .json. Al restaurar, podrás elegir qué partes recuperar.
Exportar

Genera un .json que contiene todo lo que has personalizado en el plugin.

  • Páginas pilar y reglas
  • URLs excluidas
  • Plantillas de imagen y contenido
  • Bases de conocimiento y fichas
  • Automatizaciones programadas
  • Ajustes globales
  • API key de OpenAI (seguridad)
Descargar backup .json
Restaurar

Sube un .json exportado del plugin. Te mostraremos lo que contiene y elegirás qué restaurar.

Arrastra el .json aquí
Reglas de enlazado — CSV
Exporta e importa reglas desde Excel o cualquier hoja de cálculo
Exportar reglas a CSV

Descarga todas las reglas en formato .csv compatible con Excel, Google Sheets o LibreOffice.

Descargar CSV
Importar reglas desde CSV

Sube un .csv con el formato indicado. Filas con la misma keyword actualizan la regla existente.

📋 Ver estructura requerida del CSV

La primera fila debe ser la cabecera con estas columnas:

ColumnaObligatorioDescripciónEjemplo
keyword✓Palabra exacta a enlazarseguro de salud
sinonimosSinónimos separados por | (pipe)seguro médico|cobertura
url_destino✓URL completa de destinohttps://web.com/seguros/
idiomaCódigo de idioma (es, en, fr…)es
prioridadNúmero 1–100. Por defecto: 5080
max_enlacesMáx. enlaces por post. Por defecto: 12
activo1 = activa, 0 = desactivada1

💡 El separador puede ser coma o punto y coma — se detecta automáticamente. Columnas en cualquier orden.

Arrastra el .csv aquí o haz clic
14 ) { $key_display = substr( $openai_key, 0, 7 ) . str_repeat( '•', 16 ) . substr( $openai_key, -4 ); } else { // Unusually short key; just show bullets $key_display = str_repeat( '•', max( 8, $len ) ); } } // Default tab is "Módulos" in Paid (showcases the modules system), // "enlazado" in Free (the only feature configurable from Ajustes). // The strip script removes the Paid line; Free keeps the fallback. // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( ! isset( $active_tab ) ) $active_tab = sanitize_key( wp_unslash( $_GET['tab'] ?? 'enlazado' ) ); // Back-compat redirects: keep old bookmarks working after // tab merges/removals. if ( $active_tab === 'semantico' ) { // Old "Enlazado semántico" tab → merged into "Enlazado". $active_tab = 'enlazado'; } if ( $active_tab === 'actividad' ) { // "Actividad" tab was removed entirely. if ( ! isset( $active_tab ) || $active_tab === 'actividad' ) $active_tab = 'enlazado'; } $base_url = admin_url( 'admin.php?page=flinkil-ajustes' ); // Each tab has a small SVG icon that fits the vertical sidebar layout. // Icons are minimal (no fill, stroke 2) to match the rest of the plugin's design system. // // Order is intentional and groups tabs by mental model: // Group 1 (essential): Módulos, OpenAI // Group 2 (per feature): Enlazado, Contenido e imágenes // Group 3 (advanced): Cron, General // The visual separator between groups is rendered in the loop below // by reading the '_group' marker on each tab. $tabs = array( 'enlazado' => array( 'label' => 'Enlazado', 'icon' => '', '_group' => 2, ), 'generales' => array( // Renamed "Generales" → "General" (singular). 'label' => 'General', 'icon' => '', '_group' => 3, ), ); ?>
Comportamiento del enlazado
Controla dónde y cuántos enlaces inserta el plugin
Tipos de contenido donde el plugin insertará enlaces automáticamente. Aplica tanto a reglas como al enlazado semántico. Recomendado: solo Entrada — tus páginas pilar deben quedar intactas.
true), 'objects'); $exclude = array('attachment','revision','nav_menu_item','custom_css','customize_changeset','oembed_cache','wp_block','wp_template','wp_template_part','wp_navigation','wp_font_family','wp_font_face','wp_global_styles'); ?>
name, $exclude)) continue; $checked = in_array($pt->name, $current_arr) ? 'checked' : ''; ?>
Tipos de contenido hacia los que se pueden generar enlaces. Para arquitectura piramidal SEO, marca Entrada + Página: tus posts del blog enlazarán a las páginas pilar sin que estas reciban modificaciones.
name, $exclude)) continue; $checked = in_array($pt->name, $current_dest_arr) ? 'checked' : ''; ?>
Número máximo de enlaces que las reglas de enlazado pueden insertar en un mismo post. Este límite aplica solo a los enlaces generados por reglas, no al enlazado semántico (que tiene su propio límite). Recomendado: entre 3 y 5.

Ajustes de enlazado actualizados

El cambio en el número máximo de enlaces afecta a todos los artículos. Para aplicar la nueva configuración es necesario ejecutar un rastreo completo.

Ajustes generales
Configuración general del plugin
is not allowed by WordPress.org review. } } add_action( 'admin_init', function() { register_setting( 'flink_settings', 'flink_max_enlaces_total', array( 'type' => 'integer', 'default' => 5, 'sanitize_callback' => 'absint', ) ); register_setting( 'flink_settings', 'flink_origen_tipo', array('type'=>'string','default'=>'post','sanitize_callback'=>'sanitize_text_field') ); // Separate "destino" setting: which post_types can RECEIVE links. // Default 'post,page' implements the SEO-correct topical cluster // pattern out of the box: blog posts link TO pillar pages, but // pillar pages aren't touched by the linking engine. register_setting( 'flink_settings', 'flink_destino_tipo', array('type'=>'string','default'=>'post,page','sanitize_callback'=>'sanitize_text_field') ); // General settings register_setting( 'flink_settings_general', 'flink_delete_data_on_uninstall', array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => function( $val ) { return $val ? 1 : 0; }, ) ); // The onboarding hidden flag is "1 = hidden". The setting field is // labelled positively ("Mostrar guía de Primeros pasos en el menú") // so we invert the user-facing checkbox value when sanitising: // checkbox checked → hidden=0 (show), unchecked → hidden=1 (hide). // The form submits a hidden helper field that always exists so an // unchecked checkbox is detectable. register_setting( 'flink_settings_general', 'flink_onboarding_hidden', array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => function( $val ) { return $val ? 1 : 0; }, ) ); } ); // Handle unchecked checkbox — when form submits without checkbox, set to 0 add_action( 'admin_init', function() { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( isset( $_POST['option_page'] ) && $_POST['option_page'] === 'flink_settings_content' ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( ! isset( $_POST['flink_show_predefined_templates'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $_POST['flink_show_predefined_templates'] = '0'; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( ! isset( $_POST['flink_disable_comments'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $_POST['flink_disable_comments'] = '0'; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( ! isset( $_POST['flink_seo_slug_enabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $_POST['flink_seo_slug_enabled'] = '0'; } } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( isset( $_POST['option_page'] ) && $_POST['option_page'] === 'flink_settings_general' ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( ! isset( $_POST['flink_delete_data_on_uninstall'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. $_POST['flink_delete_data_on_uninstall'] = '0'; } } }, 5 ); // Flag needs_processing when settings that affect linking are changed add_action( 'update_option_flink_max_enlaces_total', function( $old, $new ) { if ( $old != $new ) { update_option( 'flink_needs_processing', 1, false ); delete_option( 'flink_batch_reviewed' ); } }, 10, 2 ); add_action( 'update_option_flink_origen_tipo', function( $old, $new ) { if ( $old != $new ) { update_option( 'flink_needs_processing', 1, false ); delete_option( 'flink_batch_reviewed' ); } }, 10, 2 );