Como limitar el editor a ciertos bloques por rol en PHP en WordPress

Contents

Introducción

Este artículo explica con todo lujo de detalles cómo limitar qué bloques del editor (Gutenberg) pueden usar los usuarios según su rol en WordPress, implementando la lógica en PHP. Verás por qué hacerlo en el servidor es más seguro que confiar únicamente en JavaScript, cómo integrar la restricción por rol y por tipo de post, y cómo sanear el contenido al guardarlo para evitar que bloques no permitidos lleguen a la base de datos.

Por qué limitar bloques por rol y consideraciones previas

  • Usabilidad: simplifica la experiencia del editor evitando bloques no necesarios o complejos.
  • Seguridad y control editorial: evita que usuarios sin permisos inserten bloques que puedan romper la maquetación o introducir funcionalidades indeseadas.
  • Server-side vs client-side: aunque es útil ocultar bloques con JavaScript, solo la lógica server-side (PHP) garantiza que la restricción se aplique siempre (REST API, importaciones, manipulaciones directas, guest users, etc.).
  • Compatibilidad: este enfoque usa filtros nativos de WordPress (por ejemplo, allowed_block_types_all), por lo que requiere WordPress relativamente moderno (5.x).

Resumen de la estrategia

  1. Usar el filtro PHP allowed_block_types_all para restringir qué bloques se muestran en el editor según el rol del usuario y el post_type.
  2. Proveer una función central que devuelva los bloques permitidos por rol (y opcionalmente por tipo de contenido).
  3. Sanear el contenido al guardarlo (save_post o hooks REST) para eliminar bloques no permitidos si el cliente intenta enviarlos de forma manual.
  4. Opcionalmente, exponer excepciones: por ejemplo, administradores lo ven todo.

Requisitos y buenas prácticas

  • Trabaja en un plugin propio o en el archivo functions.php de un tema hijo. Recomiendo un plugin para mantener la lógica independiente del tema.
  • Haz pruebas en un entorno de desarrollo antes de aplicar en producción.
  • Documenta los nombres de bloques permitidos: los bloques core usan el formato core/nombre (por ejemplo core/paragraph) y los bloques personalizados suelen tener prefijo del plugin o tema (por ejemplo acf/mi-bloque, my-plugin/hero).
  • Si utilizas registro de bloques dinámicos o bloques que sólo existen en el frontend, asegúrate de que los nombres coincidan exactamente.

Ejemplo completo (plugin mínimo)

Este ejemplo demuestra una implementación organizada: fichero de plugin que limita bloques por rol y sanitiza al guardar.

lt?php
/
  Plugin Name: Limitar Bloques por Rol
  Description: Ejemplo que limita los bloques del editor por rol y limpia el contenido al guardar.
  Version: 1.0
  Author: Tu Nombre
 /

// Evitar acceso directo
if ( ! defined( ABSPATH ) ) {
    exit
}

/
  Devuelve un array de nombres de bloques permitidos para un usuario (por rol).
  Puedes personalizar los arrays para cada rol.
 /
function lbpr_get_allowed_blocks_for_user( user = null, post_type = null ) {
    if ( ! user ) {
        user = wp_get_current_user()
    }

    // Administradores: permisos completos (null o true para mantener comportamiento original)
    if ( in_array( administrator, (array) user->roles, true ) ) {
        return true // deja que WP muestre todo
    }

    // Mapa de bloques permitidos por rol
    bloques_por_rol = array(
        editor => array(
            core/paragraph,
            core/heading,
            core/image,
            core/list,
            core/quote,
            core/columns
        ),
        author => array(
            core/paragraph,
            core/heading,
            core/image
        ),
        contributor => array(
            core/paragraph,
            core/heading
        ),
    )

    // Ver si existe una regla por rol del usuario
    foreach ( (array) user->roles as role ) {
        if ( isset( bloques_por_rol[ role ] ) ) {
            // Posibilidad: personalizar por post_type si se necesita
            if ( post_type  page === post_type  editor === role ) {
                // ejemplo de regla específica para páginas (si se quisiera)
                return array_merge( bloques_por_rol[ role ], array( core/cover ) )
            }
            return bloques_por_rol[ role ]
        }
    }

    // Si no hay rol coincidente: conjunto muy limitado
    return array( core/paragraph, core/heading )
}

/
  Filtro que controla los bloques permitidos en el editor.
  Utiliza allowed_block_types_all (recibe 2 args).
 /
add_filter( allowed_block_types_all, lbpr_allowed_block_types_all, 10, 2 )
function lbpr_allowed_block_types_all( allowed_block_types, editor_context ) {
    // Obtener usuario actual
    user = wp_get_current_user()

    // Obtener post_type si está disponible en el contexto del editor
    post_type = isset( editor_context[post_type] ) ? editor_context[post_type] : null

    allowed_for_user = lbpr_get_allowed_blocks_for_user( user, post_type )

    // Si devolvemos true dejamos el comportamiento por defecto (todo permitido)
    if ( allowed_for_user === true ) {
        return allowed_block_types
    }

    // Si allowed_block_types viene como true (todo permitido), convertimos a listado real de bloques registrados
    if ( allowed_block_types === true ) {
        if ( class_exists( WP_Block_Type_Registry ) ) {
            registry = WP_Block_Type_Registry::get_instance()
            registered = registry->get_all_registered()
            all_names = array()
            foreach ( registered as b ) {
                if ( isset( b->name ) ) {
                    all_names[] = b->name
                }
            }
            allowed_block_types = all_names
        } else {
            // fallback: si no podemos obtener registrados, dejamos el valor por defecto
            allowed_block_types = true
        }
    }

    // Intersecar: devolver sólo los bloques permitidos para el usuario (si allowed_block_types es array)
    if ( is_array( allowed_block_types ) ) {
        return array_values( array_intersect( allowed_block_types, (array) allowed_for_user ) )
    }

    // Si algo no encaja, devolver la lista específica para el usuario
    return (array) allowed_for_user
}

/
  Sanitizar bloques al guardar para prevenir envíos maliciosos/inesperados.
  Hook en save_post y usamos parse_blocks / serialize_blocks.
 /
add_action( save_post, lbpr_sanitize_blocks_on_save, 10, 3 )
function lbpr_sanitize_blocks_on_save( post_ID, post, update ) {
    // Evitar revisiones y autosaves
    if ( wp_is_post_revision( post_ID )  ( defined( DOING_AUTOSAVE )  DOING_AUTOSAVE ) ) {
        return
    }

    // Obtener usuario que está guardando (si no hay, nada que hacer)
    user = wp_get_current_user()
    if ( empty( user->ID ) ) {
        return
    }

    post_type = post->post_type
    allowed = lbpr_get_allowed_blocks_for_user( user, post_type )

    // Si el usuario tiene permiso total (true), no sanear
    if ( allowed === true ) {
        return
    }

    // parse_blocks devuelve array de bloques
    blocks = parse_blocks( post->post_content )
    if ( empty( blocks ) ) {
        return
    }

    filtered_blocks = array()
    foreach ( blocks as block ) {
        // Bloque sin nombre (HTML libre o contenido): mantener para no eliminar HTML necesario
        if ( empty( block[blockName] ) ) {
            filtered_blocks[] = block
            continue
        }

        if ( in_array( block[blockName], allowed, true ) ) {
            filtered_blocks[] = block
            continue
        }

        // Si el bloque es anidado (innerBlocks), intentar filtrar sus hijos
        if ( ! empty( block[innerBlocks] ) ) {
            block[innerBlocks] = lbpr_filter_inner_blocks( block[innerBlocks], allowed )
            // Si tiene ahora contenido, lo mantenemos
            if ( ! empty( block[innerBlocks] ) ) {
                filtered_blocks[] = block
            }
        }
        // Si no está permitido y no tiene contenido válido, lo descartamos.
    }

    new_content = serialize_blocks( filtered_blocks )

    // Si no hay cambios, salir
    if ( new_content === post->post_content ) {
        return
    }

    // Actualizar contenido: evitar recursión quitando temporalmente el hook
    remove_action( save_post, lbpr_sanitize_blocks_on_save, 10 )
    wp_update_post( array(
        ID => post_ID,
        post_content => new_content,
    ) )
    add_action( save_post, lbpr_sanitize_blocks_on_save, 10, 3 )
}

/
  Función auxiliar para filtrar innerBlocks recursivamente.
 /
function lbpr_filter_inner_blocks( blocks, allowed ) {
    out = array()
    foreach ( blocks as block ) {
        if ( empty( block[blockName] )  in_array( block[blockName], allowed, true ) ) {
            if ( ! empty( block[innerBlocks] ) ) {
                block[innerBlocks] = lbpr_filter_inner_blocks( block[innerBlocks], allowed )
            }
            out[] = block
        } else {
            // si no permitido, intentar filtrar sus hijos
            if ( ! empty( block[innerBlocks] ) ) {
                filtered_children = lbpr_filter_inner_blocks( block[innerBlocks], allowed )
                if ( ! empty( filtered_children ) ) {
                    block[innerBlocks] = filtered_children
                    out[] = block
                }
            }
        }
    }
    return out
}
?gt

Resumen del código

  • lbpr_get_allowed_blocks_for_user: función central que decide qué bloques están permitidos según rol y opcionalmente por post_type.
  • Filtro allowed_block_types_all: controla qué bloques aparecen en el editor.
  • save_post: sanea el contenido antes de guardar, eliminando bloques no permitidos accidentalmente insertados o enviados por REST.

Personalizaciones y detalles útiles

  • Permitir bloques de un plugin o tema: incluye su nombre exacto (namespace/block-name) en los arrays de bloques permitidos.
  • Reglas por post_type: dentro de lbpr_get_allowed_blocks_for_user puedes detectar post_type y devolver reglas diferentes (por ejemplo, páginas pueden permitir core/cover).
  • Basado en capacidades en lugar de roles: en vez de roles, puedes usar current_user_can(edit_pages) o capacidades personalizadas para decidir. Esto es más flexible en sitios con roles personalizados.
  • Bloques dinámicos y shortcodes: si tienes bloques dinámicos que renderizan contenido en PHP, asegúrate de mantener su nombre en la lista cuando corresponda.
  • Depuración: para depurar qué devuelve allowed_block_types_all, añade temporalmente registros con error_log o use WP_DEBUG_LOG.

Ejemplos rápidos adicionales

Permitir todo menos un bloque concreto

Si quieres que un rol pueda usar todos los bloques excepto, por ejemplo, core/freeform o core/cover, puedes construir la lista de todos los bloques registrados y filtrar.

add_filter( allowed_block_types_all, function( allowed, context ) {
    user = wp_get_current_user()
    if ( in_array( editor, (array) user->roles, true ) ) {
        // obtener todos los bloques registrados y eliminar core/cover
        if ( class_exists( WP_Block_Type_Registry ) ) {
            registry = WP_Block_Type_Registry::get_instance()
            registered = registry->get_all_registered()
            all = array()
            foreach ( registered as b ) {
                all[] = b->name
            }
            return array_diff( all, array( core/cover ) )
        }
    }
    return allowed
}, 10, 2 )

Usar capacidades en lugar de roles

function ejemplo_por_capacidad( allowed, context ) {
    if ( current_user_can( manage_options ) ) {
        return allowed // administradores
    }
    if ( current_user_can( edit_posts ) ) {
        return array( core/paragraph, core/heading, core/image )
    }
    return array( core/paragraph )
}
add_filter( allowed_block_types_all, ejemplo_por_capacidad, 10, 2 )

Pruebas y verificación

  1. Instala el plugin en un entorno de desarrollo y actívalo.
  2. Crea o edita contenido con usuarios de distintos roles y verifica que el selector de bloques muestra solo los bloques permitidos.
  3. Intenta inyectar un bloque no permitido mediante la REST API o pegando HTML/JSON de otro editor y guarda: comprueba que la sanitización en save_post elimina el bloque no permitido.
  4. Revisa logs si algo no funciona y confirma los nombres exactos de bloques con el inspector del editor o listando WP_Block_Type_Registry.

Advertencias finales

  • Si devuelves literalmente true en allowed_block_types_all estás indicando todo permitido — úsalo con cuidado para administradores únicamente.
  • El filtrado en el cliente (JS) es útil para UX pero no es seguro por sí solo la sanitización en servidor evita que contenido no autorizado se persista.
  • Si tu sitio usa caching a nivel de objeto o páginas, asegúrate de que no caches respuestas que dependan del usuario mostrando bloques distintos.

Conclusión

Limitar los bloques por rol en PHP es una estrategia robusta para mantener coherencia editorial y control en sitios WordPress. Utilizando el filtro allowed_block_types_all junto con una sanitización al guardar obtienes una solución completa: buena experiencia de usuario y seguridad a nivel servidor. El ejemplo incluido es adaptativo y puede ampliarse según necesidades (post types, capacidades, excepciones, etc.).



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *