ViewState для 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 такой функции, к сожалению, нет,  так что приходится прибегать к таким выкрутасам.  Приведем логическую конструкцию:
  1. Начало скрипта (inlude,require)
  2. Обработка GET/POST, если они есть.
  3. Вывод данных (HTML)
  4. Конец
Рассмотрим аспекты более детально. Нам необходим самый простой метод работы с данными. Можно, например, использовать ключевой массив ($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 которые вам нужно непосредственно запихнуть в &lt;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