Bug Tracker: добавление записей и комментариев (часть седьмая)

Владимир | | CodeIgniter, HTML, MySQL, PHP, Web разработка.

В этой части цикла статей о разработке баг трекера мы рассмотрим добавление записей о багах и комментариев к ним.

Вообще-то обе эти операции сводятся к добавлению записей в таблицы bugs и comments, т.е. их можно выполнить с помощью всего пары строк кода.

Но, как несложно догадаться, основная часть работы будет заключаться в обработке данных.

Кроме того, нужно сразу решить вопрос с тегами и защитой от XSS атак.

Проблема следующая. Если разрешить пользователям вставлять в текст описания багов любые HTML теги и не выполнять никаких проверок, то кто угодно сможет провести любую XSS атаку. Например, вставить скрипт с редиректом на свой ресурс.

С другой стороны, если фильтровать все теги, то посетители не смогут использовать жирный шрифт, курсив и т.п.

Решить проблему можно с помощью bbCodes или фильтрацией части тегов. Например, теги script удаляем, а strong – не трогаем.

Я решил выбрать второй вариант. Тем более что его не сложно реализовать с помощью библиотеки HTML Purifier. Кроме фильтрации опасных тегов, библиотека исправляет ошибки в разметке. Например, добавляет закрывающие теги.

Подключение HTML Purifier к CodeIgniter.

Я использовал вот эту инструкцию. Правда, я создал для библиотеки отдельную папку (\application\libraries\htmlpurifier).

После этого нужно закомментировать строку
require 'HTMLPurifier.php';
в файле HTMLPurifier.includes.php.

А в файл HTMLPurifier.php (сразу после <?php) добавить
require_once('HTMLPurifier.includes.php');

Подключается библиотека так:

$this->load->library('htmlpurifier/HTMLPurifier');

Теперь рассмотрим алгоритм добавления записи о баге.

add bug

Сначала мы проверяем данные с помощью встроенной библиотеки form_validation. Для её работы нужно установить правила для каждого поля формы.

Полей у нас 5: заголовок, имя пользователя, eMail пользователя, категория и описание бага.

Если проверка прошла, мы обрабатываем данные с помощью HTML Purifier и добавляем их в базу. Читаем из сессии адрес последней страницы, на которой находился посетитель. И отправляем ему редирект с адресом этой страницы. Т.к. у нас есть несколько страниц, на которых находится форма добавления бага (главная, страницы категорий), то пользователь, скорее всего, захочет остаться на той странице, с которой он отправлял сообщение.

Если во время проверки возникли ошибки – формируем страницу с этой же формой и описаниями ошибок и отправляем её посетителю.

Теперь посмотрите на код метода addbug.

function addbug() {
	$this->load->library('form_validation');
	$this->form_validation->set_error_delimiters('<div class="fErrMessage">', '</div>');
	$this->form_validation->set_rules('title', 'lang:title', 'required');
	$this->form_validation->set_rules('uname', 'lang:uname', 'required');
	$this->form_validation->set_rules('category_id', 'lang:category_id', 'required|integer');
	$this->form_validation->set_rules('description', 'lang:description', 'required');
	$this->form_validation->set_rules('email', 'lang:email', 'required|valid_email');
	if ($this->form_validation->run() === FALSE) {
		//ошибка
		$pageData['message'] = 'Форма заполнена неправильно';
		
		//показываем форму с описаниями ошибок
		$pageData['title'] = 'Bug Tracker';
		$pageData['categories'] = $this->mcategory->getAllCategories();
		
		$this->load->view('header', $pageData);
		$this->load->view('categories');
		$this->load->view('addbugform');
		$this->load->view('footer');
	}
	else {
		//форма заполнена правильно
		$this->load->library('htmlpurifier/HTMLPurifier');
		$config = HTMLPurifier_Config::createDefault();
		
		$bugData['title'] = $this->htmlpurifier->purify($this->input->post('title'));
		$bugData['uname'] = $this->htmlpurifier->purify($this->input->post('uname'));
		$bugData['category_id'] = $this->input->post('category_id');
		$bugData['uemail'] = $this->input->post('email');
		$bugData['description'] = $this->htmlpurifier->purify($this->input->post('description'));

		if (($res = $this->mbug->addNewBug($bugData)) !== FALSE) {
			//данные добавлены
			$this->session->set_flashdata('message', 'Сообщение добавлено');
		}
		else {
			//при добавлении данных возникла ошибка
			$this->session->set_flashdata('message', 'При добавлении данных возникла ошибка');
		}
		
		redirect($this->session->userdata('prev_page'));
	}
}

Как видите, метод работает точно в соответствии с описанным алгоритмом.

Тут стоит обратить внимание на строки 6 и 8. В них мы установили правила (integer, valid_email), которые обеспечивают проверку номера категории и email адреса.

Кроме того, использовать библиотеку HTML Purifier имеет смысл только после обычной проверки. Причин тут две. Во-первых, если пользователь не заполнил какое-то поле формы (а в данном случае они все обязательные), то пытаться удалить теги из него бессмысленно. Во-вторых, если вы все-таки попытаетесь это сделать, то HTML Purifier начинает потреблять кучу ресурсов. Например, использование памяти увеличивается с ~2,6 до 7 МБ.

Ещё один интересный момент связан с использованием сессий. Для вставки сообщений о результатах выполнения операции мы используем метод set_flashdata. Особенность flash данных в том, что они автоматически удаляются после считывания.

Таким образом, посетитель увидит сообщение о добавлении бага только один раз. Если он обновит страницу, то сообщение исчезнет.

Рассмотрим представление, которое создает форму (файл application\views\addbugform.php)

<?php
if (!empty($errMessage)) {
	echo '<div id="errMessage">'.$errMessage.'</div>';
}
$mes = $this->session->flashdata('message');
if (!empty($mes)) {
	echo '<div id="infoMessage">'.$mes.'</div>';
}
?>
<?php echo form_open('bugtracker/addbug', array('id'=>'fAddBug')); ?>
	<?php echo form_error('title'); ?>
	<p>
	<label for="title">Заголовок</label>
	<input type="text" name="title" id="title" value="<?php echo set_value('title'); ?>" />
	</p>
	<?php echo form_error('uname'); ?>
	<p>
	<label for="uname">Ваше имя</label>
	<input type="text" name="uname" id="uname" value="<?php echo set_value('uname'); ?>" />
	</p>
	<?php echo form_error('category_id'); ?>
	<p>
	<label for="category_id">Категория ошибки</label>
	<select name="category_id" id="category_id" size="1">
	<?php
		foreach ($categories as $category) {
			echo '<option value="'.$category['id'].'"';
			echo set_select('category_id', $category['id']);
			echo '>'.$category['name'].'</option>';
		}
	?>
	</select>
	</p>
	<?php echo form_error('email'); ?>
	<p>
	<label for="email">eMail</label>
	<input type="text" name="email" id="email" value="<?php echo set_value('email'); ?>" />
	</p>
	<?php echo form_error('description'); ?>
	<p>
	<label for="description">Описание ошибки</label>
	<textarea name="description" id="description" cols="30" rows="5"><?php echo set_value('description'); ?></textarea>
	</p>
	<p><input type="submit" name="addbug" id="addbug" value="Отправить" />
	</p>
</form>

В начале представления мы выводим сообщения, если они есть. После этого создаем форму.

Форма в общем-то обычная. Обратить внимание стоит на функции вывода ошибок form_error и автоматического заполнения формы set_value. Обратите внимание на восстановление предыдущего значения Select Box (строка 28). В первом параметре метода set_select нужно указать значение атрибута name Select Box.

Теперь переходим к модели (application\models\ mbug.php).

function addNewBug($bugData) {
	$qAddBug = 'INSERT INTO bugs (title, uname, category_id, description, bug_date, uemail)'
		.' VALUES (?, ?, ?, ?, NOW(), ?)';
	$res = $this->db->query($qAddBug, array(
					$bugData['title'],
					$bugData['uname'],
					$bugData['category_id'],
					$bugData['description'],
					$bugData['uemail']
	));
	if ($res) {
		return $this->db->insert_id();
	}
	return $res;
}

Здесь всё просто. В параметре $bugData передается массив с данными из формы. После этого выполняется вставка этих данных в таблицу bugs.

При выполнении запроса все знаки вопроса заменяются исходными данными. При этом автоматически происходит экранирование спецсимволов, что обеспечивает защиту от SQL Injection.

Добавление комментариев.

Этот метод практически не отличается от предыдущего. Тот же алгоритм, те же библиотеки.

Поэтому приводить код для этих методов я не буду.

Конечно в форме только 3 поля: имя, email и текст комментария. Правил проверки меньше. И, конечно, данные вставляются в таблицу comments, а не bugs.

Но на один момент хочу обратить ваше внимание. На данный момент (отсутствует поддержка JavaScript) можно оставить комментарий только первого уровня. Т.е. нельзя указать, что ваш комментарий является ответом на предыдущий.

Когда будет добавлена поддержка JavaScript, форма отправки комментария будет работать также как и в этом блоге, т.е. перемещаться под выбранный комментарий. При этом будет добавляться скрытое поле, содержащее id комментария на который вы отвечаете.

Сделать ответы на предыдущие комментарии без JavaScript можно, но при этом придется добавлять ещё один select box, содержащий перечень предыдущих комментариев, и пользователь сам должен будет выбрать, на какой комментарий он отвечает. Естественно, это очень неудобно. Поэтому поддержка ответов на предыдущие комментарии будет только при поддержке JavaScript.

До встречи!

P.S. Если возникли вопросы, пишите, попробую ответить 😉