Как создать "виртуальную" страницу в 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. Я провозился с этим целый час, прежде чем догадался, что нужно обновить постоянные ссылки... Если кто-то знает функцию, которая может это сделать?
ethanpil
Для внешнего правила: Поскольку путь к корню моего сайта содержал пробел, это вызывало ошибку в Apache. Пробелы в пути к вашей WordPress-установке необходимо экранировать.
Willster
Работает, но я не могу получить доступ к переданным переменным запроса через get_query_vars() в my-api.php. Я проверил, какие переменные загружаются. Единственная установленная переменная - это WP object под названием $wp. Как мне получить доступ или преобразовать это в объект WP_Query, чтобы я мог получить доступ к переданным переменным через get_query_vars()?
Jules
@Jules: Когда вы include файл, он выполняется в текущей области видимости. В данном случае это функция wpse9870_parse_request, которая имеет только параметр $wp. Вполне возможно, что глобальный объект $wp_query ещё не установлен в этот момент, поэтому get_query_var() не будет работать. Однако вам повезло: $wp — это класс, который содержит нужный вам член query_vars — я сам использую его в приведённом выше коде.
Jan Fabry
@JanFabry Спасибо. Разобрался с этим спустя некоторое время. Мне просто было интересно, есть ли способ преобразовать объект WP в объект WP_Query. Но я не уверен, что в этом вообще есть необходимость, так что не берите в голову. Спасибо за этот и все остальные кусочки кода. Уже нашёл много полезной информации на этом сайте благодаря вам.
Jules
пытаюсь создать внешние правила перезаписи. добавил ваш первый фрагмент кода, но всё равно получаю 404. кстати: сбросил правила перезаписи
Sisir
Я также продолжаю получать ошибку 404 с внешними правилами перезаписи. Просто для уточнения, приведенный выше код должен быть добавлен в основной PHP-файл плагина, а не в файл функций шаблонов, верно?
Lee
@ethanpil вы можете (теперь?) сбросить правила, чтобы включить ваши новые правила перезаписи, если правила перезаписи WP их не включают. Метод описан здесь http://codex.wordpress.org/Class_Reference/WP_Rewrite
Ejaz
В моем случае это работает на URL index.php?wpse9870_api=1 и также my-api.php?wpse9870_api=1. Как я могу удалить строку запроса?
er.irfankhan11
@Irfan Я пытаюсь достичь того же самого, не могли бы вы подсказать, что мне следует написать в my-api.php?
Prafulla Kumar Sahu
@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
er.irfankhan11
Действительно ли внутреннее правило хранится в базе данных? Выполняет ли add_rewrite_rule INSERT в базу данных? Похоже, что оно просто хранится в исходном коде.
Jeff
Извините за нубский вопрос, но... куда должен идти вызов add_action( 'init'...? Я поместил его в метод __construct() моего плагина, но callback-метод никогда не выполняется. Пробовал перезапускать сервер и т.д.
T Nguyen
Пытаюсь использовать External rule, но получаю ошибку 403 при попытке прямого доступа. Стандартный .htaccess в wp-content блокирует доступ к php-файлу в директории моего плагина, и ни одно из добавленных мной правил .htaccess для разрешения доступа не работает. Есть советы?
AutoBaker
Это сработало у меня. Я никогда не трогаю 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' );
Jules
Есть ли причина не делать что-то подобное?
Затем просто подключите свой плагин к хуку 'init' и проверьте наличие этой GET-переменной. Если она существует, выполните необходимые действия в вашем плагине и завершите выполнение с помощью die()
Это сработает, но я пытаюсь обеспечить четкое различие между переменными запроса и самим конечным эндпоинтом. В будущем могут появиться другие аргументы запроса, и я не хочу, чтобы пользователи их путали.
EAMann
Что если оставить перезапись, но перенаправлять её в GET-переменную? Также можно посмотреть, как работает перезапись для robots.txt. Это может помочь понять, как избежать перенаправления к my-api.php/
Will Anderson
Возможно, я не до конца понимаю ваш вопрос, но может ли простая шорткод-функция решить вашу проблему?
Шаги:
- Попросите клиента создать страницу, например: 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. Время покажет, был ли я прав!