WordPress как на ладони
Недорогой хостинг для сайтов на WordPress: wordpress.jino.ru

SHORTINIT константа: WordPress среда с минимальной нагрузкой

Меня волновал вопрос: как можно использовать объект $wpdb и Базу Данных сайта с которой я работаю, но чтобы при этом минимально подгружать среду WP. Иногда бывает нужно, при использовании Ajax получить или записать данные в БД и ничего больше: не нужны фильтры, не нужна проверка на авторизацию пользователя, не нужны функции WоrdPress, не нужны всякие проверки и куча подгружаемых функций. В общем, не нужно ничего кроме возможности общаться с Базой Данных привычными для WordPress методами.

Решить такую задачу можно, считав данные подключения к БД из файла wp-config.php и отдельно подключится к БД. Но это не очень удобно и потребует лишнего кода, который по сути уже есть в файлах WordPress. А то получатся очередные костыли.

С версии 3.4 разработчики WordPress позаботились об этом и добавили константу SHORTINIT в wp-settings.php:

// Останавливаем основную загрузку WordPress если нам нужна только база.
if ( SHORTINIT )
	return false;

Работает она так:

// указываем, что нам нужен минимум от WP
define('SHORTINIT', true);

// подгружаем среду WordPress
// WP делает некоторые проверки и подгружает только самое необходимое для подключения к БД
require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php' );

// тут мы можем общаться с БД. Но практически никакие функции WP работать не будут.
// Глобальные переменные $wp, $wp_query, $wp_the_query не установлены...
global $wpdb;
$result = $wpdb->get_results("SELECT post_title FROM $wpdb->posts WHERE post_type='post'");

if( $result )
	foreach( $result as $post ){
		echo "$post->post_title <br>";
	}

Конкретные числа

Чтобы посмотреть чем отличаются инициализации с SHORTINIT и без. Я замерил: количество SQL запросов, время на выполнение кода и используемая память. Вот что получилось:

require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php' );
// 5 SQL за 0.1 сек., память: 14.92 mb

define( 'SHORTINIT', true );
require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php' );
// 0 SQL за 0.02 сек., память: 2.35 mb

Таким образом SHORTINIT, грубо, по всем параметрам снижает нагрузку в 5 раз. Неплохо!

Что работает при SHORTINIT

При использовании SHORTINIT система фильтров: apply_filters() do_action() уже работает. Фильтры прописаны основные (файл: /wp-includes/default-filters.php) Те что вы указывали в functions.php вашей темы и многие другие работать не будут.

Из привычных функций: esc_attr(), is_single(), the_content(), get_permalink() и т.д. не работает ни одна. Вот все функции которые подключаются:

Авторизация при SHORTINIT

Пример проверки авторизации и получения всех прав пользователя с SHORTINIT

В комментариях Александр поделился кодом, который исправно работает, за что ему отдельное спасибо. Далее код бы доработан Михаилом (обернут в функцию) и вот что получилось в итоге:

GitHub
<?php

// указываем, что нам нужен минимум от WP
define('SHORTINIT', true);

// подгружаем среду WordPress
require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp/wp-load.php' );

// укороченная версия wp_hash из pluggable.php
function wp_hash( $data ){
	$salt = LOGGED_IN_KEY . LOGGED_IN_SALT;
	return hash_hmac('md5', $data, $salt);
}

function hash_token( $token ){
	if( function_exists( 'hash' ) )
		return hash( 'sha256', $token );
	else
		return sha1( $token );
}

function curr_user_can($capability){
	global $all_caps;
	return isset( $all_caps[$capability] ) && $all_caps[$capability];
}

function get_curr_user_can(){
	global $wpdb;
	$_options = $wpdb->get_results("SELECT `option_name`, `option_value` FROM $wpdb->options WHERE `option_name` IN ('siteurl', '".$wpdb->prefix."user_roles')", 'OBJECT_K');
	if(!$_options)
		return array( 'error', 'Опции не найдены' );

	$_c_hash = md5( $_options['siteurl']->option_value );
	if( !isset( $_COOKIE['wordpress_logged_in_'.$_c_hash] ) )
		return array( 'error', 'Кука отсутствует' );

	$cookie          = $_COOKIE['wordpress_logged_in_'.$_c_hash];
	$cookie_elements = explode('|', $cookie);
	if( count( $cookie_elements ) !== 4 )
		return array( 'error', 'Кука битая' );

	$username   = $cookie_elements[0];
	$expiration = $cookie_elements[1];
	$token      = $cookie_elements[2];
	$hmac       = $cookie_elements[3];

	if( $expiration < time() )
		return array( 'error', 'Время сессии истекло' );

	$user = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->users WHERE `user_login`=%s", $username) , 'OBJECT' );
	if( ! $user )
		return array( 'error', 'Нет такого юзера' );

	$pass_frag = substr($user->user_pass, 8, 4);
	$key       = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token );
	$algo      = function_exists( 'hash' ) ? 'sha256' : 'sha1';
	$hash      = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, $key );
	if( ! hash_equals( $hash, $hmac ) )
		return array( 'error', 'Хэш не эквивалентен' );

	$user_options = $wpdb->get_results("SELECT `meta_key` ,`meta_value` FROM $wpdb->usermeta WHERE (`user_id`=".$user->ID.") AND (`meta_key` IN ('session_tokens', '".$wpdb->prefix."capabilities') )", OBJECT_K );
	if(!$user_options)
		return array( 'error', 'Юзер опции не установлены' );

	$sessions = unserialize($user_options['session_tokens']->meta_value);
	$verifier = hash_token( $token );
	if( !isset( $sessions[ $verifier ] ) )
		return array( 'error', 'Токен авторизации отсутствует' );

	if( $sessions[$verifier]['expiration'] < time() )
		return array( 'error', 'Время сессии истекло' );

	$role_caps = unserialize( $_options[ $wpdb->prefix.'user_roles' ]->option_value );
	$user_caps = unserialize( $user_options[ $wpdb->prefix.'capabilities' ]->meta_value );
	$all_caps  = array();
	foreach($user_caps as $key => $value){
		if( isset($role_caps[$key]) && $value )
			$all_caps = array_merge( $all_caps, $role_caps[$key]['capabilities'] );
		else
			$all_caps[$key] = $value;
	}
	return array( 'success', $all_caps );
}

Первый протестированный вариант кода

Код тестировался на WP 4.5. В ранних или поздних версиях возможно нужно будет изменить проверку...

GitHub
<?php

// указываем, что нам нужен минимум от WP
define('SHORTINIT', true);

// подгружаем среду WordPress
require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp/wp-load.php' );

// укороченная версия wp_hash из pluggable.php
function wp_hash($data) {
	$salt = LOGGED_IN_KEY . LOGGED_IN_SALT;
	return hash_hmac('md5', $data, $salt);
}

function hash_token( $token ) {
	if ( function_exists( 'hash' ) ) {
		return hash( 'sha256', $token );
	} else {
		return sha1( $token );
	}
}

global $wpdb;

// получаем значение siteurl и список ролей из БД, неудачно - прерываем выполнение скрипта
$_options = $wpdb->get_results("SELECT `option_name`, `option_value` FROM $wpdb->options WHERE `option_name` IN ('siteurl', '".$wpdb->prefix."user_roles')", 'OBJECT_K');
if (!$_options) exit;

// получаем md5 хеш для siteurl - он формирует ключ куки
$_c_hash = md5( $_options['siteurl']->option_value );

if ( !isset( $_COOKIE['wordpress_logged_in_'.$_c_hash] ) ) exit;

// получаем параметр, разбиваем на элементы
$cookie = $_COOKIE['wordpress_logged_in_'.$_c_hash];
$cookie_elements = explode('|', $cookie);

// кол-во элементов = 4, если нет - параметр куки поврежден, прерываем скрипт
if ( count( $cookie_elements ) !== 4 ) exit;
list( $username, $expiration, $token, $hmac ) = $cookie_elements;

// проверяем время жизни куки, если истекло - прерываем скрипт
if ( $expiration &lt; time() ) exit; 

// получаем данные о пользователе из БД по логину, не удалось - прерываем скрипт
$user = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->users WHERE `user_login`=%s", $username) , 'OBJECT' );
if ( ! $user ) exit;

$pass_frag = substr($user->user_pass, 8, 4);
$key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token );

$algo = function_exists( 'hash' ) ? 'sha256' : 'sha1';
$hash = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, $key );
// хеш код из куки не совпал с вычисленным - прерываем скрипт
if ( ! hash_equals( $hash, $hmac ) ) exit;

// проверяем сессию
// получаем сессии пользователя и доп. права по user ID из usermeta, не получили - прерываем скрипт
$user_options = $wpdb->get_results("SELECT `meta_key` ,`meta_value` FROM $wpdb->usermeta WHERE (`user_id`=".$user->ID.") AND (`meta_key` IN ('session_tokens', '".$wpdb->prefix."capabilities') )", OBJECT_K );
if (!$user_options) exit;

$sessions = unserialize($user_options['session_tokens']->meta_value);
$verifier = hash_token( $token );

// сессия не найдена или устарела - прерываем скрипт
if ( isset( $sessions[ $verifier ] ) ) {
	if ( $sessions[$verifier]['expiration'] &lt; time() ) exit; 
} else exit;

// наборы прав для ролей и пользователя
$role_caps = unserialize( $_options[ $wpdb->prefix.'user_roles' ]->option_value );
$user_caps = unserialize( $user_options[ $wpdb->prefix.'capabilities' ]->meta_value );
$all_caps = array();
// формируем общий набор прав пользователя
foreach ($user_caps as $key => $value){
	//$key есть в наборе ролей - значит, это роль 
	if ( isset($role_caps[$key]) && $value ) 
		$all_caps = array_merge( $all_caps, $role_caps[$key]['capabilities'] );
	// это ключ-право пользователя
	else
		$all_caps[$key] = $value;
}

// аналог current_user_can()
function curr_user_can($capability){
	global $all_caps;
	return isset( $all_caps[$capability] ) && $all_caps[$capability];
}

// все проверки пройдены, можно выполнять запросы

// массив прав текущего пользователя
print_r( $all_caps );
37 комментов
Полезные 3 Все
  • @ Андрей

    После выхода ВП версии 5.2 у меня перестало работать подгрузка функционала ВП через

    require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php' );

    То есть в 5.1 все это работало отлично, а в 5.2 как отрубило. Ни в логах ВП, ни в логах сервера ошибок не вижу. Есть у кого-то мысли почему и как теперь жить с этим?

    1
    Ответить4 мес назад #
    • Kama7602

      В новой версии подгрузка ядра никак не менялась, логика таже. Очень странно что у тебя так, попробуй поискать причину в чем-то еще... У меня допустим есть подобные подгрузки и работают как и работали на 5.2.

      1
      Ответить4 мес назад #
      • @ Андрей

        Да уж к сожалению что мог попробовал. Откатываю ВП на 5.1 - все снова работает, возвращаю 5.2 - нет. Ставлю флаги проверки - код не выполняется именно дальше инклюда. Туда типа уходит и с концами (

        Ответить4 мес назад #
        • Kama7602

          Значит при такой загрузке возникает PHP ошибка какая-то, может тема/плагин. Дебаг включи, в логи загляни... Очень сомневаюсь что это сам ВП, может несовместимость плагина какого-то с новой версией ВП вызывает ошибку и поэтому все ломается...

          Ответить4 мес назад #
  • Alexandrov Yan27 codyshop.ru

    Если использовать подключение:

    require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php' );

    Плагин не принимают в репозиторий WordPress.

    И вообще, если его планируете размещать туда, от SHORTINIT придётся отказаться.

    1
    Ответить3 мес назад #
    • Kama7602

      Да, это скорее для каких-то частных дел подойдет, редко нужно такое.

      1
      Ответить3 мес назад #
      • Alexandrov Yan27 codyshop.ru

        Я вот подумал. Почему при SHORTINIT = true нужно подключать wp-load.php?

        Ведь судя по статье https://wp-kama.ru/handbook/wordpress/loading он раньше загрузился, чем идёт проверка константы.

        Интересно, можно ли реализовать аналог SHORTINIT, с тем, чтобы в репо приняли? Судя по тому, что мне написал ревьюер, подключение wp-load.php запрещено из соображения безопасности и стабильности.

        Ответить3 мес назад #
        • Kama7602

          Я вот подумал. Почему при SHORTINIT = true нужно подключать wp-load.php?

          Не понял, а как по-другому? Зачем вообще эту константу определять, если не подключить wp-load.php?

          1) определил SHORTINIT = true
          2) подключил wp-load.php
          3) сработала только начальная часть всей загрузки ВП (только БД, система хуков)

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

          Интересно, можно ли реализовать аналог SHORTINIT, с тем, чтобы в репо приняли?

          Я думаю нельзя, там у них стандарт, даже если по логике все будет четко все равно не примут - стандарт непозволяет! Все аякс запросы например, через admin-ajax.php или рест должны делать и больше никак...

          Ответить3 мес назад #
Здравствуйте, !     Войти . Зарегистрироваться