Как создать "виртуальную" страницу в WordPress
Я пытаюсь создать пользовательский API эндпоинт в WordPress, и мне нужно перенаправлять запросы к виртуальной странице в корне WordPress на реальную страницу, которая идет с моим плагином. По сути, все запросы к одной странице должны перенаправляться на другую.
Пример:
http://mysite.com/my-api.php
=> http://mysite.com/wp-content/plugins/my-plugin/my-api.php
Цель этого - сделать URL для API эндпоинта максимально коротким (аналогично http://mysite.com/xmlrpc.php
), но при этом хранить реальный файл API эндпоинта в плагине, а не требовать от пользователя перемещать файлы в их установке или изменять ядро WordPress.
Моя первая попытка заключалась в добавлении пользовательского правила перезаписи URL. Однако это вызвало две проблемы:
- Эндпоинт всегда имел завершающий слеш. Получалось
http://mysite.com/my-api.php/
- Мое правило перезаписи применялось только частично. Вместо перенаправления на
wp-content/plugins...
, оно перенаправляло наindex.php&wp-content/plugins...
. Это приводило к тому, что WordPress либо показывал ошибку "страница не найдена", либо просто отображал главную страницу.
Есть идеи? Предложения?

В WordPress существует два типа правил перезаписи: внутренние правила (хранятся в базе данных и обрабатываются функцией WP::parse_request()) и внешние правила (хранятся в .htaccess
и обрабатываются Apache). Вы можете выбрать любой вариант в зависимости от того, насколько вам необходим функционал WordPress в вызываемом файле.
Внешние правила:
Внешние правила проще всего настроить и понять. Они выполняют файл my-api.php
в директории вашего плагина, не загружая ничего из 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 );
// Шаблон начинается с символа '^'
// Подстановка начинается с "корня сайта", по крайней мере с '/'
// Это эквивалентно добавлению к `non_wp_rules`
$wp_rewrite->add_external_rule( 'my-api.php$', $plugin_url );
}
Внутренние правила:
Внутренние правила требуют больше работы: сначала мы добавляем правило перезаписи, которое добавляет переменные запроса, затем делаем эту переменную запроса публичной, и, наконец, проверяем её наличие для передачи управления нашему файлу плагина. К этому моменту обычная инициализация WordPress уже произойдет (мы прерываем выполнение прямо перед стандартным запросом постов).
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;
}

Хочу добавить, что важно зайти на страницу Постоянных ссылок и нажать "Сохранить изменения" в админке WordPress. Я провозился с этим целый час, прежде чем догадался, что нужно обновить постоянные ссылки... Если кто-то знает функцию, которая может это сделать?

Для внешнего правила: Поскольку путь к корню моего сайта содержал пробел, это вызывало ошибку в Apache. Пробелы в пути к вашей WordPress-установке необходимо экранировать.

Работает, но я не могу получить доступ к переданным переменным запроса через get_query_vars()
в my-api.php. Я проверил, какие переменные загружаются. Единственная установленная переменная - это WP object
под названием $wp
. Как мне получить доступ или преобразовать это в объект WP_Query
, чтобы я мог получить доступ к переданным переменным через get_query_vars()
?

@Jules: Когда вы include
файл, он выполняется в текущей области видимости. В данном случае это функция wpse9870_parse_request
, которая имеет только параметр $wp
. Вполне возможно, что глобальный объект $wp_query
ещё не установлен в этот момент, поэтому get_query_var()
не будет работать. Однако вам повезло: $wp
— это класс, который содержит нужный вам член query_vars
— я сам использую его в приведённом выше коде.

@JanFabry Спасибо. Разобрался с этим спустя некоторое время. Мне просто было интересно, есть ли способ преобразовать объект WP в объект WP_Query. Но я не уверен, что в этом вообще есть необходимость, так что не берите в голову. Спасибо за этот и все остальные кусочки кода. Уже нашёл много полезной информации на этом сайте благодаря вам.

пытаюсь создать внешние правила перезаписи. добавил ваш первый фрагмент кода, но всё равно получаю 404. кстати: сбросил правила перезаписи

Я также продолжаю получать ошибку 404 с внешними правилами перезаписи. Просто для уточнения, приведенный выше код должен быть добавлен в основной PHP-файл плагина, а не в файл функций шаблонов, верно?

@ethanpil вы можете (теперь?) сбросить правила, чтобы включить ваши новые правила перезаписи, если правила перезаписи WP их не включают. Метод описан здесь http://codex.wordpress.org/Class_Reference/WP_Rewrite

В моем случае это работает на URL index.php?wpse9870_api=1
и также my-api.php?wpse9870_api=1
. Как я могу удалить строку запроса?

@Irfan Я пытаюсь достичь того же самого, не могли бы вы подсказать, что мне следует написать в my-api.php?

@Prafulla Kumar Sahu Я использую вот так: `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;
}`
и URL выглядит так: http://homeurl/?getrequest

Действительно ли внутреннее правило хранится в базе данных? Выполняет ли add_rewrite_rule
INSERT в базу данных? Похоже, что оно просто хранится в исходном коде.

Извините за нубский вопрос, но... куда должен идти вызов add_action( 'init'...
? Я поместил его в метод __construct()
моего плагина, но callback-метод никогда не выполняется. Пробовал перезапускать сервер и т.д.

Пытаюсь использовать External rule, но получаю ошибку 403 при попытке прямого доступа. Стандартный .htaccess в wp-content блокирует доступ к php-файлу в директории моего плагина, и ни одно из добавленных мной правил .htaccess для разрешения доступа не работает. Есть советы?

Это сработало у меня. Я никогда не трогаю Rewrite API, но всегда готов двигаться в новых направлениях. Следующий код работал на моем тестовом сервере для WordPress 3.0, расположенном в подпапке localhost. Не вижу проблем, если WordPress установлен в корень веб-сайта.
Просто поместите этот код в плагин и загрузите файл с именем "taco-kittens.php" напрямую в папку плагинов. Вам потребуется выполнить жесткий сброс постоянных ссылок. Думаю, лучшее время для этого — активация плагина.
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' );
С наилучшими пожеланиями, -Майк

Я получил ошибку "доступ запрещен" при попытке использовать этот код. Подозреваю, что мой сервер или WordPress не приняли абсолютный URL. С другой стороны, этот код сработал нормально: add_rewrite_rule( 'taco-kittens', 'wp-content/plugins/taco-kittens.php', 'top' );

Есть ли причина не делать что-то подобное?
Затем просто подключите свой плагин к хуку 'init' и проверьте наличие этой GET-переменной. Если она существует, выполните необходимые действия в вашем плагине и завершите выполнение с помощью die()

Это сработает, но я пытаюсь обеспечить четкое различие между переменными запроса и самим конечным эндпоинтом. В будущем могут появиться другие аргументы запроса, и я не хочу, чтобы пользователи их путали.

Что если оставить перезапись, но перенаправлять её в GET-переменную? Также можно посмотреть, как работает перезапись для robots.txt. Это может помочь понять, как избежать перенаправления к my-api.php/

Возможно, я не до конца понимаю ваш вопрос, но может ли простая шорткод-функция решить вашу проблему?
Шаги:
- Попросите клиента создать страницу, например: http://mysite.com/my-api
- Попросите клиента добавить шорткод на эту страницу, например: [my-api-shortcode]
Эта новая страница будет выступать в качестве API-эндпоинта, а ваш шорткод будет отправлять запросы к коду вашего плагина по адресу http://mysite.com/wp-content/plugins/my-plugin/my-api.php
(разумеется, это означает, что в файле my-api.php должен быть определён этот шорткод)
Шаги 1 и 2 можно, вероятно, автоматизировать через плагин.

Я еще не так много работал с перезаписью URL, поэтому это решение, вероятно, немного грубовато, но оно работает:
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() );
}
Это работает, если подключить функцию к хуку 'generate_rewrite_rules', но должно быть лучшее решение, так как нежелательно перезаписывать .htaccess при каждой загрузке страницы.
Кажется, я не могу перестать редактировать свои собственные сообщения... вероятно, лучше поместить это в callback активации плагина и использовать глобальную переменную $wp_rewrite. А затем удалить запись из non_wp_rules и снова записать в .htaccess в callback деактивации.
И наконец, запись в .htaccess должна быть более продуманной — нужно заменять только секцию WordPress в файле.

У меня было похожее требование, и я хотел создать несколько конечных точек на основе уникальных слагов, которые указывали бы на контент, генерируемый плагином.
Посмотрите исходный код моего плагина: https://wordpress.org/extend/plugins/picasa-album-uploader/
Используемая мной техника начинается с добавления фильтра для the_posts
, чтобы анализировать входящий запрос. Если плагин должен его обработать, создается фиктивный пост и добавляется действие для template_redirect
.
Когда вызывается действие template_redirect
, оно должно либо вывести все содержимое страницы для отображения и завершить выполнение, либо вернуться без генерации вывода. Посмотрите код в wp_include/template-loader.php
, и вы поймете почему.

Я использую другой подход, который заключается в принудительной загрузке главной страницы с пользовательским заголовком, содержимым и шаблоном страницы.
Это решение очень аккуратное, так как может быть реализовано, когда пользователь переходит по дружественной ссылке, например http://example.com/?plugin_page=myfakepage
Очень легко реализовать и позволяет создавать неограниченное количество страниц.
Код и инструкции здесь: Создание пользовательской/фейковой/виртуальной страницы WordPress на лету

Это готовый пример для продакшена, сначала создаем класс виртуальной страницы:
class VirtualPage
{
private $query;
private $title;
private $content;
private $template;
private $wp_post;
function __construct($query = '/index2', $template = 'page', $title = 'Без названия')
{
$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;
// Обновляем основной запрос
$wp_query->current_post = $this->wp_post->ID;
$wp_query->found_posts = 1;
$wp_query->is_page = true;//важная часть
$wp_query->is_singular = true;//важная часть
$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); // добавляем случайное число чтобы избежать конфликтов
$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;
}
}
На следующем шаге подключаем действие template_redirect
и обрабатываем виртуальную страницу как показано ниже
add_action( 'template_redirect', function () {
switch ( get_query_var( 'name' ,'') ) {
case 'contact':
// http://вашсайт/contact ==> загружает page-contact.php
$page = new VirtualPage( "/contact", 'contact',__('Свяжитесь со мной') );
$page->createPage();
break;
case 'archive':
// http://вашсайт/archive ==> загружает page-archive.php
$page = new VirtualPage( "/archive", 'archive' ,__('Архивы'));
$page->createPage();
break;
case 'blog':
// http://вашсайт/blog ==> загружает page-blog.php
$page = new VirtualPage( "/blog", 'blog' ,__('Блог'));
$page->createPage();
break;
}
} );

Я использую подход, аналогичный тому, что описал выше Xavi Esteve, который перестал работать из-за обновления WordPress, насколько я мог понять, во второй половине 2013 года.
Это подробно описано здесь: https://stackoverflow.com/questions/17960649/wordpress-plugin-generating-virtual-pages-and-using-theme-template
Ключевая часть моего подхода — использование существующего шаблона, чтобы итоговая страница выглядела как часть сайта; я хотел, чтобы она была максимально совместима со всеми темами, желательно и в будущих версиях WordPress. Время покажет, был ли я прав!
