ViewState для PHP
php,
эксперименты
В ASP.NET есть обалденная вещь и называется она ViewState. HTTP связь с сервером как правило не поддерживает отношений между данными так что приходится извращаться с данными в форму, куки, сессию и пр. Однако некоторые вещи порой не подходят для реализации как, например куки и сессии которые одинаковы на все приложение. Единственное что можно сделать, это внести в форму (<form>) дополнительные скрытые поля, которые при запросе обратно к скрипту дадут ему понять, что делать с данными. В ASP.NET эта функция встроена и поддерживает сохранение любых объектов помеченных атрибутом
Serializable в том числе и примитивных типов как string, Guid, и т.д. Пример свойства с сохранением идентификатора статьи:
Guid MyId {
get{
return (ViewState["MyId"]!=null) ? (Guid)ViewState["MyId"] : Guid.Empty;
}
set{
ViewState["MyId"] = value;
}
}
Однако в PHP такой функции, к сожалению, нет, так что приходится прибегать к таким выкрутасам. Приведем логическую конструкцию:
- Начало скрипта (inlude,require)
- Обработка GET/POST, если они есть.
- Вывод данных (HTML)
- Конец
Рассмотрим аспекты более детально. Нам необходим самый простой метод работы с данными. Можно, например, использовать ключевой массив ($array['key']) практически, как и в ASP.NET. Облегчает ситуацию еще и возможность сериализировать любой объект в php в строку. Потом всю эту строку еще и скормим в base64. Далее:
$myarray = array("1"=>"foo", "2"=>"bar");
$data = base64_encode(serialize($myarray));
Получается, что все, что нам нужно будет сделать, это поставить эту строку в input type="hidden" и результат готов. При следующей загрузки страницы просто-напросто конвертируем обратно от base64 и десиарилизируем. Лучше всего сделать это в ООП подходе и создать класс.
Ах да, вы наверное, хотели спросить про безопасность. Известно, что при малейшем изменении данных, хэш SHA критически меняется. Но вот как зафиксировать попытку изменения данных в форме кулцхакера? :). Можно хэшировать сериализованную строку (перед base64) и сохранить хэш в сессии. Однако если открыто много форм, получится много значений и в сессии. Придется при следующем запросе чистить память. Ключ к значению сессии можно хранить как раз во вью-стейте. Ничего с ним не случится а если попытаются его подменить на какой-нибудь жизненно важный объект, то произойдет облом в проверке данных. В итоге у нас и код безопасный, и передача данных безопасна. Все, что остается делать это инклюдить и юзать.
В конце-концов относим все в отдельный класс, viewstate.php:
<?php
if(!defined("INCLUDE")) die("Вам тут делать нечего");
/**
* Основной класс ViewState для убогой реализации штатной передачи данных
* @author crypton / crypton@crypton-technologies.net
*
*/
class ViewState {
/**
* Многоярусный массив где хранятся все данные вью стейта
* @var array
*/
public $VIEWSTATE = array();
/**
* Проверка подлинности вьюстейта. Лучше оставить это включенным по техники безопасности что-бы разные кулцхакеры не ковырялись
* @var boolean
*/
public $ValidateRequest = true;
private $VS_KEY = NULL;
/*
* Генерация ключа проверки. Если хотите, можете настроить диапазон
*/
private $RANDKEY_START = 11;
private $RANDKEY_END = 22717;
/**
* Основной конструктор вьюстейта. Эта функция автоматом вызовет loadFromPostBack() если это нужно
* @return unknown_type
*/
function __construct() {
$this->loadFromPostBack();
}
/**
* Вызывайте эту функцию в начале вашего скрипта. Эта функция тупо загружает данные обратно из вьюстейта. Функция также возвращает тру или фалс
* в зависимости от успеха декодирования вьюстейта (в том числе и проверки его подлинности)
* @return unknown_type
*/
public function loadFromPostBack(){
if(count($_POST) == 0 || !isset($_POST['__VIEWSTATE'])) {
return;
}
$data = base64_decode($_POST['__VIEWSTATE']);
if(!$data) {
trigger_error("Ошибка валидации ViewState. Входные данные повреждены. [base64_decode]", E_USER_WARNING);
return false;
}
$vsdata = unserialize($data);
/*
* Формат на самом деле-то такой вот:
* Array =>
* [VALIDATIONKEY] => ключь валидации в сессии, генерируется каждый раз случайно
* [VIEWSTATE] => сами данные вьюстейта как массив который вам вздумается *
*/
if(!$vsdata) {
trigger_error("Ошибка валидации ViewState. Входные данные повреждены. [unserialize]", E_USER_WARNING);
return false;
}
if(!isset($vsdata['VALIDATIONKEY']) || !isset($vsdata['VIEWSTATE'])) {
// хм.. странновато получается
trigger_error("Ошибка валидации ViewState. Входные данные повреждены. [arraykey-notexist]", E_USER_WARNING);
return false;
}
// проверить данные вью стейта чтоб в нем не ковырялись
$fromkey = $vsdata['VALIDATIONKEY'];
if($this->ValidateRequest){
if(!isset($_SESSION[$fromkey])) {
trigger_error("Ошибка валидации ViewState. Входные данные не прошли проверки. [session]", E_USER_WARNING);
return false;
}
// мы просто берем хэш полученного вьюстейта (сериализованного) и сверяем его
$PrevKey = $_SESSION[$fromkey];
$ThisKey = sha1($data);
if($PrevKey != $ThisKey){
// упс, кто-то поковырялся, надо их мухобойкой :)
trigger_error("Ошибка валидации ViewState. Входные данные не прошли проверки. [keymismatch]", E_USER_WARNING);
return false;
}
}
// очистить сессию от лишнего мусора
unset($_SESSION[$fromkey]);
// вернуть все на круги своя
$this->VIEWSTATE = $vsdata['VIEWSTATE'];
return true;
}
/**
* Эта функция возвращает данные в base64 которые вам нужно непосредственно запихнуть в <input type=hidden name=__VIEWSTATE.
* @return string
*/
public function getData(){
// сгенерируем ключь валидации вью-стейта
if($this->VS_KEY == NULL){
$ok = false;
// генерируем число если нет ключа в сессии. в лучшем случае этот цикл пройдет всего-лишь раз
while(!$ok){
$this->VS_KEY = mt_rand($this->RANDKEY_START, $this->RANDKEY_END);
if(!isset($_SESSION['VS_'.$this->VS_KEY])){
$ok=true;
}
}
}
// построить массив в нашем формате
$vsdata = array(
'VALIDATIONKEY' => 'VS_'.$this->VS_KEY,
'VIEWSTATE' => $this->VIEWSTATE
);
$data = serialize($vsdata);
$hash = sha1($data);
$_SESSION['VS_'.$this->VS_KEY] = $hash;
$base64 = base64_encode($data);
return $base64;
}
}
?>
А в основной странице,
<?php
/*
* Основная декларация текущего пути. Дабы избежать некоторых конфликтов ("багов", гы) в PHP когда он находит разные файлы с одним и тем-же
* именем, в разных папках
*/
define("ROOT_PATH", realpath('./'));
/*
* Декларация для других инклюдов, которые без неё будут посылать пользователя нах**
*/
define("INCLUDE",true);
/*
* С декларациями разобрались, теперь делаем самое главное. Допустим, что после скобки закрывающей этот камент у вас начинается
* любой PHP файл с формой, где надо реализовать ВьюСтейт. Делаем ваши любимые инклюды и заинклюдываем наш класс по работе с вьюстейтом
*/
include ROOT_PATH."/viewstate/viewstate.php";
session_start();
// инициализируем, как по правилу, вьюстейт
$ViewState = new ViewState();
// мы можем загрузить данные из вьюстейта, если они конечно у нас есть. для этого можно поставить какой-нибудь магический ключь при
// наличии которого можно определить если вьюстейт загружен от POST или загружается чисто страница
$mydata = "";
extract($ViewState->VIEWSTATE); // данная функция тупо перезапишет переменные чуть выше. короче смотрите мануал http://docs.php.net/manual/en/function.extract.php
// тут у нас основной код проги, ля-ля-ля
$errors = array();
if(count($_POST)) {
$mydata = htmlentities($_POST['mydata']);
echo "Полученные данные (POST): <br /><pre>";
print_r($_POST);
echo "</pre><br />Данные ViewState (с прошлого запроса):<br /><pre>";
print_r($ViewState->VIEWSTATE);
echo "</pre><br />Данные сессии:<br /><pre>";
print_r($_SESSION);
echo "</pre>";
// загружаем данные обратно в вьюстейт ЕСЛИ нажата первая кнопка
if($_POST['act'] == 'Сохранить текст')
$ViewState->VIEWSTATE['mydata'] = $mydata;
}
?>
<html>
<head>
<title>Эксперимент с грубой эмуляцией ViewStae</title>
</head>
<body>
<p>Введите текст, нажмите отправить, и посмотрите на исходный код
страницы. ViewState будет сохранять различные переменные в себе.</p>
<fieldset><legend>Эмуляция вьюстейта</legend>
<form action="<?php echo $_SERVER['PHP_SELF']?>" method="POST"><textarea
name="mydata" cols="50" rows="10"></textarea> <input type="submit" name="act" value="Сохранить текст" />
<input type="submit" name="act" value="Тупо отправить форму" />
<input type="hidden" name="__VIEWSTATE" value="<?php echo $ViewState->getData() ?>" />
</form>
</fieldset>
</body>
</html>
А лучше не копи-пасть, а скачай весь архив: php-viewstate.zip