Cómo crear una página "virtual" en WordPress
Estoy intentando crear un endpoint API personalizado en WordPress, y necesito redirigir las solicitudes a una página virtual en la raíz de WordPress a una página real que viene con mi plugin. Básicamente, todas las solicitudes a una página deben ser redirigidas a la otra.
Ejemplo:
http://misitio.com/mi-api.php
=> http://misitio.com/wp-content/plugins/mi-plugin/mi-api.php
El objetivo de esto es hacer la URL del endpoint API lo más corta posible (similar a http://misitio.com/xmlrpc.php
) pero incluir el archivo real del endpoint API con el plugin en lugar de requerir que el usuario mueva archivos en su instalación o modifique el núcleo.
Mi primer intento fue agregar una regla de reescritura personalizada. Sin embargo, esto tuvo dos problemas:
- El endpoint siempre tenía una barra diagonal al final. Se convertía en
http://misitio.com/mi-api.php/
- Mi regla de reescritura solo se aplicaba parcialmente. No redirigía a
wp-content/plugins...
, sino aindex.php&wp-content/plugins...
. Esto hacía que WordPress mostrara un error de página no encontrada o simplemente mostrara la página de inicio.
¿Ideas? ¿Sugerencias?

Existen dos tipos de reglas de reescritura en WordPress: reglas internas (almacenadas en la base de datos y analizadas por WP::parse_request()), y reglas externas (almacenadas en .htaccess
y analizadas por Apache). Puedes elegir cualquiera de las dos opciones, dependiendo de cuánto de WordPress necesites en tu archivo llamado.
Reglas externas:
La regla externa es la más fácil de configurar y seguir. Ejecutará my-api.php
en el directorio de tu plugin, sin cargar nada de WordPress.
add_action( 'init', 'wpse9870_init_external' );
function wpse9870_init_external()
{
global $wp_rewrite;
$plugin_url = plugins_url( 'my-api.php', __FILE__ );
$plugin_url = substr( $plugin_url, strlen( home_url() ) + 1 );
// El patrón tiene como prefijo '^'
// La sustitución tiene como prefijo la "raíz del sitio", al menos un '/'
// Esto es equivalente a añadirlo a `non_wp_rules`
$wp_rewrite->add_external_rule( 'my-api.php$', $plugin_url );
}
Reglas internas:
La regla interna requiere algo más de trabajo: primero añadimos una regla de reescritura que agrega variables de consulta, luego hacemos pública esta variable de consulta, y finalmente necesitamos verificar la existencia de esta variable de consulta para pasar el control a nuestro archivo de plugin. Para cuando hacemos esto, la inicialización habitual de WordPress ya habrá ocurrido (interrumpimos justo antes de la consulta regular de entradas).
add_action( 'init', 'wpse9870_init_internal' );
function wpse9870_init_internal()
{
add_rewrite_rule( 'my-api.php$', 'index.php?wpse9870_api=1', 'top' );
}
add_filter( 'query_vars', 'wpse9870_query_vars' );
function wpse9870_query_vars( $query_vars )
{
$query_vars[] = 'wpse9870_api';
return $query_vars;
}
add_action( 'parse_request', 'wpse9870_parse_request' );
function wpse9870_parse_request( &$wp )
{
if ( array_key_exists( 'wpse9870_api', $wp->query_vars ) ) {
include 'my-api.php';
exit();
}
return;
}

Solo quiero agregar que es importante ir a la página de Enlaces permanentes y hacer clic en "Guardar cambios" en el WP-Admin. Estuve probando esto durante una hora antes de pensar que necesitaba actualizar los enlaces permanentes... A menos que alguien conozca una función que pueda hacer eso?

Para la regla externa: Debido a que la ruta a mi raíz web tenía un carácter de espacio en blanco, esto hizo que Apache fallara. Los espacios en blanco deben escaparse si existen en la ruta a tu instalación de WordPress.

Funciona, pero no puedo acceder a las variables de consulta pasadas con get_query_vars()
en my-api.php. Verifiqué qué variables están cargadas. Y la única variable que está configurada es un objeto WP
llamado $wp
. ¿Cómo puedo acceder o transformar esto en un objeto WP_Query
para poder acceder a las variables pasadas con get_query_vars()
?

@Jules: Cuando incluyes (include
) un archivo, este se ejecuta en el ámbito actual. En este caso, es la función wpse9870_parse_request
, que solo tiene el parámetro $wp
. Es posible que el objeto global $wp_query
no se haya establecido en este momento, por lo que get_query_var()
no funcionará. Sin embargo, tienes suerte: $wp
es la clase que contiene el miembro query_vars
que necesitas - yo mismo lo uso en el código anterior.

@JanFabry Gracias. Lo descubrí después de un rato. Solo me preguntaba si hay una manera de convertir un objeto WP a un objeto WP_Query. Pero no estoy muy seguro de si alguna vez habría necesidad de algo así, así que no importa. Gracias por esto y por todos tus otros fragmentos de código. Ya he encontrado mucha información útil en este sitio gracias a ti.

intentando crear reglas de reescritura externas. agregué tu primer fragmento de código pero sigo recibiendo 404. por cierto: limpié las reglas de reescritura

Todavía estoy recibiendo un error 404 con las reglas de reescritura externas. Solo para aclarar, el código anterior va en el archivo PHP principal del plugin, no en el archivo de funciones de plantillas, ¿correcto?

@ethanpil puedes (¿ahora?) limpiar las reglas para incluir tus nuevas reglas de reescritura si las reglas de reescritura de WP no las incluyen. El método está documentado aquí http://codex.wordpress.org/Class_Reference/WP_Rewrite

En mi caso funciona en la URL index.php?wpse9870_api=1
y también en my-api.php?wpse9870_api=1
¿Cómo puedo eliminar la cadena de consulta?

@Irfan Estoy intentando lograr lo mismo, ¿puedes decirme qué debería escribir en mi my-api.php?

@Prafulla Kumar Sahu Estoy usando algo así: `add_filter( 'query_vars', 'wpse9870_query_vars' ); function wpse9870_query_vars( $query_vars ) { $query_vars[] = 'getrequest'; return $query_vars; }
add_action( 'parse_request', 'wpse9870_parse_request' );
function wpse9870_parse_request( &$wp )
{
if ( array_key_exists( 'getrequest', $wp->query_vars ) ) {
include 'my-api.php';
exit();
}
return;
}`
y la url es http://homeurl/?getrequest

¿La regla interna realmente se almacena en la base de datos? ¿add_rewrite_rule
hace un INSERT en la base de datos? Parece que solo se almacena en el código fuente.

Perdón por la pregunta de novato pero... ¿dónde debería ir la llamada add_action( 'init'...
? La puse en el método __construct()
de mi plugin, pero el método callback nunca se ejecuta. He intentado reiniciar el servidor, etc.

Intentando usar la regla Externa pero obtengo un error 403 cuando intento acceder directamente. El .htaccess por defecto en wp-content está bloqueando el acceso al archivo php en mi directorio de plugin, y ninguna de las reglas .htaccess que he añadido para permitir el acceso funciona. ¿Algún consejo?

Esto funcionó para mí. Nunca toco la API de reescritura, pero siempre estoy dispuesto a empujarme en nuevas direcciones. Lo siguiente funcionó en mi servidor de prueba para 3.0 ubicado en una subcarpeta de localhost. No preveo ningún problema si WordPress está instalado en la raíz web.
Solo coloca este código en un plugin y sube el archivo llamado "taco-kittens.php" directamente en la carpeta de plugins. Necesitarás forzar un hard flush para tus enlaces permanentes. Creo que dicen que el mejor momento para hacer esto es en la activación del plugin.
function taco_kitten_rewrite() {
$url = str_replace( trailingslashit( site_url() ), '', plugins_url( '/taco-kittens.php', __FILE__ ) );
add_rewrite_rule( 'taco-kittens\\.php$', $url, 'top' );
}
add_action( 'wp_loaded', 'taco_kitten_rewrite' );
Los mejores deseos, -Mike

Recibí un error de acceso denegado al intentar este código. Sospecho que a mi servidor o a WordPress no les gustó la URL absoluta. Esto, por otro lado, funcionó bien: add_rewrite_rule( 'taco-kittens', 'wp-content/plugins/taco-kittens.php', 'top' );

¿Alguna razón para no hacer algo como esto en su lugar?
Luego simplemente engancha tu plugin en el hook 'init' y verifica esa variable GET. Si existe, haz lo que tu plugin necesite hacer y usa die()

Eso funcionaría, pero estoy intentando proporcionar una distinción muy clara entre las variables de consulta y el endpoint real. Podría haber otros argumentos de consulta en el futuro, y no quiero que los usuarios confundan las cosas.

¿Qué tal si mantienes la reescritura, pero la reescribes a la variable GET? También podrías ver cómo funciona la reescritura para robots.txt. Podría ayudarte a descubrir cómo evitar la redirección a my-api.php/

Puede que no esté entendiendo completamente tus preguntas, pero ¿un simple shortcode resolvería tu problema?
Pasos:
- Haz que el cliente cree una página, por ejemplo http://mysite.com/my-api
- Haz que el cliente agregue un shortcode en esa página, por ejemplo [my-api-shortcode]
La nueva página actúa como un punto final de API y tu shortcode envía solicitudes al código de tu plugin en http://mysite.com/wp-content/plugins/my-plugin/my-api.php
(por supuesto, esto significa que my-api.php tendría definido el shortcode)
Probablemente puedas automatizar los pasos 1 y 2 a través del plugin.

No he trabajado mucho con reescrituras todavía, así que esto probablemente sea un poco rudimentario, pero parece funcionar:
function api_rewrite($wp_rewrite) {
$wp_rewrite->non_wp_rules['my-api\.php'] = 'wp-content/plugins/my-plugin/my-api.php';
file_put_contents(ABSPATH.'.htaccess', $wp_rewrite->mod_rewrite_rules() );
}
Funciona si enganchas esto al hook 'generate_rewrite_rules', pero debe haber una mejor manera, ya que no quieres reescribir el .htaccess en cada carga de página.
Parece que no puedo dejar de editar mis propias publicaciones... probablemente debería ir en tu callback de activación y hacer referencia a global $wp_rewrite en su lugar. Y luego eliminar la entrada de non_wp_rules y volver a escribir en .htaccess en tu callback de desactivación.
Y finalmente, la escritura en .htaccess debería ser un poco más sofisticada, querrás reemplazar solo la sección de WordPress allí.

Tuve un requerimiento similar y quería crear varios endpoints basados en slugs únicos que apuntaran a contenido generado por el plugin.
Echa un vistazo al código fuente de mi plugin: https://wordpress.org/extend/plugins/picasa-album-uploader/
La técnica que utilicé comienza añadiendo un filtro para the_posts
para examinar la solicitud entrante. Si el plugin debe manejarla, se genera un post ficticio y se añade una acción para template_redirect
.
Cuando se llama a la acción template_redirect
, debe resultar en la salida de todo el contenido de la página a mostrar y finalizar, o debe retornar sin generar ninguna salida. Mira el código en wp-include/template-loader.php
y entenderás por qué.

Estoy utilizando otro enfoque que consiste en forzar a la página de inicio para que cargue un título personalizado, contenido y plantilla de página.
La solución es muy limpia ya que se puede implementar cuando un usuario sigue un enlace amigable como http://ejemplo.com/?plugin_page=myfakepage
Es muy fácil de implementar y debería permitir páginas ilimitadas.
Código e instrucciones aquí: Generar una página personalizada/falsa/virtual de Wordpress sobre la marcha

Es un ejemplo listo para producción, primero crea la clase de página virtual:
class VirtualPage
{
private $query;
private $title;
private $content;
private $template;
private $wp_post;
function __construct($query = '/index2', $template = 'page', $title = 'Sin título')
{
$this->query = filter_var($query, FILTER_SANITIZE_URL);
$this->setTemplate($template);
$this->setTitle($title);
}
function getQuery()
{
return $this->query;
}
function getTemplate()
{
return $this->template;
}
function getTitle()
{
return $this->title;
}
function setTitle($title)
{
$this->title = filter_var($title, FILTER_SANITIZE_STRING);
return $this;
}
function setContent($content)
{
$this->content = $content;
return $this;
}
function setTemplate($template)
{
$this->template = $template;
return $this;
}
public function updateWpQuery()
{
global $wp, $wp_query;
// Actualizar la consulta principal
$wp_query->current_post = $this->wp_post->ID;
$wp_query->found_posts = 1;
$wp_query->is_page = true;//parte importante
$wp_query->is_singular = true;//parte importante
$wp_query->is_single = false;
$wp_query->is_attachment = false;
$wp_query->is_archive = false;
$wp_query->is_category = false;
$wp_query->is_tag = false;
$wp_query->is_tax = false;
$wp_query->is_author = false;
$wp_query->is_date = false;
$wp_query->is_year = false;
$wp_query->is_month = false;
$wp_query->is_day = false;
$wp_query->is_time = false;
$wp_query->is_search = false;
$wp_query->is_feed = false;
$wp_query->is_comment_feed = false;
$wp_query->is_trackback = false;
$wp_query->is_home = false;
$wp_query->is_embed = false;
$wp_query->is_404 = false;
$wp_query->is_paged = false;
$wp_query->is_admin = false;
$wp_query->is_preview = false;
$wp_query->is_robots = false;
$wp_query->is_posts_page = false;
$wp_query->is_post_type_archive = false;
$wp_query->max_num_pages = 1;
$wp_query->post = $this->wp_post;
$wp_query->posts = array($this->wp_post);
$wp_query->post_count = 1;
$wp_query->queried_object = $this->wp_post;
$wp_query->queried_object_id = $this->wp_post->ID;
$wp_query->query_vars['error'] = '';
unset($wp_query->query['error']);
$GLOBALS['wp_query'] = $wp_query;
$wp->query = array();
$wp->register_globals();
}
public function createPage()
{
if (is_null($this->wp_post)) {
$post = new stdClass();
$post->ID = -99;
$post->ancestors = array(); // 3.6
$post->comment_status = 'closed';
$post->comment_count = 0;
$post->filter = 'raw';
$post->guid = home_url($this->query);
$post->is_virtual = true;
$post->menu_order = 0;
$post->pinged = '';
$post->ping_status = 'closed';
$post->post_title = $this->title;
$post->post_name = sanitize_title($this->template); // añadir número aleatorio para evitar colisión
$post->post_content = $this->content ?: '';
$post->post_excerpt = '';
$post->post_parent = 0;
$post->post_type = 'page';
$post->post_status = 'publish';
$post->post_date = current_time('mysql');
$post->post_date_gmt = current_time('mysql', 1);
$post->modified = $post->post_date;
$post->modified_gmt = $post->post_date_gmt;
$post->post_password = '';
$post->post_content_filtered = '';
$post->post_author = is_user_logged_in() ? get_current_user_id() : 0;
$post->post_content = '';
$post->post_mime_type = '';
$post->to_ping = '';
$this->wp_post = new WP_Post($post);
$this->updateWpQuery();
@status_header(200);
wp_cache_add(-99, $this->wp_post, 'posts');
}
return $this->wp_post;
}
}
En el siguiente paso engancha la acción template_redirect
y maneja tu página virtual como se muestra a continuación
add_action( 'template_redirect', function () {
switch ( get_query_var( 'name' ,'') ) {
case 'contact':
// http://tusitio/contact ==> carga page-contact.php
$page = new VirtualPage( "/contact", 'contact',__('Contáctame') );
$page->createPage();
break;
case 'archive':
// http://tusitio/archive ==> carga page-archive.php
$page = new VirtualPage( "/archive", 'archive' ,__('Archivos'));
$page->createPage();
break;
case 'blog':
// http://tusitio/blog ==> carga page-blog.php
$page = new VirtualPage( "/blog", 'blog' ,__('Blog'));
$page->createPage();
break;
}
} );

Estoy utilizando un enfoque similar al de Xavi Esteve mencionado anteriormente, que dejó de funcionar debido a una actualización de WordPress, según pude notar en la segunda mitad de 2013.
Está documentado con gran detalle aquí: https://stackoverflow.com/questions/17960649/wordpress-plugin-generating-virtual-pages-and-using-theme-template
La parte clave de mi enfoque es utilizar la plantilla existente para que la página resultante parezca que es parte del sitio; quería que fuera lo más compatible posible con todos los temas, con suerte a través de las versiones de WordPress. ¡El tiempo dirá si estaba en lo correcto!
