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() і т.д. не працює жодна. Ось всі функції, які підключаються – див. /wp-settings.php :

Включаються файли, необхідні для ініціалізації.

Load early WordPress files.

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

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

У коментарях Олександр поділився кодом, який справно працює, за що йому окреме спасибі. Далі код би доопрацьований Михайлом (обернуть у функцію) і ось що вийшло в результаті:

GitHub

<?php

// specify that we need a minimum from WP
define( 'SHORTINIT', true );

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

// A shortened version of wp_hash from 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 ['error', 'Options not found'];
	}

	$_c_hash = md5( $_options['siteurl']->option_value );
	if( ! isset( $_COOKIE[ "wordpress_logged_in_$_c_hash" ] ) ){
		return [ 'error', 'No cookies.' ];
	}

	$cookie = $_COOKIE["wordpress_logged_in_$_c_hash"];
	$cookie_elements = explode('|', $cookie);
	
	if( count( $cookie_elements ) !== 4 ){
		return [ 'error', 'Cookie is broken' ];
	}

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

	if( $expiration < time() ){
		return [ 'error', 'Session time has expired' ];
	}

	$user = $wpdb->get_row( $wpdb->prepare(
		"SELECT * FROM $wpdb->users WHERE `user_login`=%s", $username ), 'OBJECT'
	);

	if( ! $user ){
		return [ 'error', 'There is no such user' ];
	}

	$ 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 [ 'error', 'Hash is not equivalent to' ];
	}

	$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 [ 'error', 'User options are not set' ];
	}

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

	if( ! isset( $sessions[ $verifier ] ) )){
		return [ 'error', 'No authorization token' ];
	}

	if( $sessions[ $verifier ]['expiration'] < time() ){
		return [ 'error', 'Session time has expired' ];
	}

	$role_caps = unserialize( $_options[ $wpdb->prefix . 'user_roles' ]->option_value );
	$user_caps = unserialize( $user_options[ $wpdb->prefix . 'capabilities' ]->meta_value );
	$all_caps = [];

	foreach( $user_caps as $key => $value ){
		if( isset( $role_caps[ $key ] ) && $value ){
			$all_caps = array_merge( $all_caps, $role_caps[ $key ]['capabilities'], true );
		}
		else {
			$all_caps[ $key ] = $value;
		}
	}

	return ['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 - він формує ключ cookie
$_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 < 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'] < 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);

Залишити коментар

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *