
Rich Text Editor для ASP.NET своими руками: iframe, designMode и execCommand
Визуальный редактор для комментариев или описаний товаров можно собрать за час на встроенных возможностях браузера. Разбираем, как он устроен и где у самодельного решения пределы.
Не путать с RichTextBox из VB6
У разработчиков на классическом VB слово «Rich Text» ассоциируется с элементом RichTextBox и форматом RTF. В вебе всё иначе: редактор работает в браузере, а результатом его работы является HTML. Поэтому и возможности другие: нельзя просто вставить картинку из буфера как двоичные данные, зато ссылки, списки и форматирование текста поддерживаются естественно.
Важно и то, что такой редактор пишется на JavaScript, а не на ASP.NET. Серверная часть только принимает готовый HTML и сохраняет его. Значит, тот же редактор можно подключить к любой серверной платформе, а ASP.NET остаётся лишь получателем данных.
Основа: iframe в режиме редактирования
Браузеры умеют превращать документ в редактируемый: достаточно установить свойство designMode в значение "on". Чаще всего для этого используют iframe, потому что он даёт изолированный документ со своими стилями: оформление сайта не смешивается с оформлением редактируемого текста.
Уже в таком виде редактор работает: Ctrl+B, Ctrl+I и Ctrl+U включают жирный, курсив и подчёркивание, а браузер сам строит разметку. Включать designMode нужно после загрузки iframe, иначе в некоторых браузерах свойство не применится.
<div class="toolbar">
<button type="button" onclick="cmd('bold')"><b>Ж</b></button>
<button type="button" onclick="cmd('italic')"><i>К</i></button>
<button type="button" onclick="cmd('insertUnorderedList')">Список</button>
<button type="button" onclick="addLink()">Ссылка</button>
</div>
<iframe id="editorFrame" title="Текст сообщения"
style="width:100%;height:240px;border:1px solid #ccc"></iframe>
<asp:HiddenField ID="hfContent" runat="server" />
<asp:Button ID="btnSave" runat="server" Text="Сохранить"
OnClientClick="syncEditor();" OnClick="btnSave_Click" />Панель инструментов и execCommand
Кнопки панели вызывают метод execCommand документа внутри iframe. Он применяет команду к текущему выделению: bold, italic, underline, insertOrderedList, createLink, insertImage, justifyCenter и другие. Старые примеры для Internet Explorer работали через document.selection.createRange(), но execCommand на уровне документа поддерживают все браузеры, и отдельная ветка для IE не нужна.
Перед командой полезно вернуть фокус в iframe: если пользователь нажал кнопку, фокус ушёл на неё, и часть браузеров применит команду к пустоте.
var frame = document.getElementById('editorFrame');
var doc;
frame.onload = init;
init();
function init() {
doc = frame.contentWindow.document;
doc.designMode = 'on';
}
function cmd(name, value) {
frame.contentWindow.focus(); // фокус обратно в редактор
doc.execCommand(name, false, value || null);
}
function addLink() {
var url = prompt('Адрес ссылки', 'https://');
if (url && /^https?:\/\//i.test(url)) cmd('createLink', url);
}
// Перед отправкой формы копируем HTML в скрытое поле
function syncEditor() {
document.getElementById('<%= hfContent.ClientID %>').value = doc.body.innerHTML;
}Передача текста на сервер
Содержимое iframe не входит в форму, поэтому само по себе на сервер не попадёт. Перед отправкой его копируют в скрытое поле, как в функции syncEditor выше. На сервере текст берётся из HiddenField.Value.
Здесь же вас встретит защита ASP.NET: проверка запроса (ValidateRequest) считает HTML в поле потенциальной атакой и бросает HttpRequestValidationException. Отключать её для всей страницы опасно. Варианты: кодировать текст на клиенте перед отправкой и раскодировать на сервере, либо отключить проверку только для этой страницы и обязательно очищать HTML вручную.
Безопасность: фильтрация HTML
Всё, что приходит из редактора, нужно считать враждебным. Злоумышленнику не нужен ваш редактор: он отправит в поле любой HTML, включая script, атрибуты onerror и ссылки вида javascript:. Если сохранить такой текст и показать его другим пользователям, получится хранимая XSS-уязвимость.
Надёжная защита строится по белому списку: разрешаются только известные теги (p, b, i, ul, li, a) и атрибуты (href с проверкой схемы), всё остальное удаляется. Регулярные выражения для этого не подходят, HTML нужно разбирать парсером.
- Белый список тегов и атрибутов, а не чёрный.
- Для ссылок разрешать только схемы http, https и mailto.
- Очищать HTML на сервере при сохранении, а не только на клиенте.
- Ограничивать длину текста, чтобы не принимать мегабайты мусора.
Где проходит граница самодельного решения
Простой редактор хорошо справляется с жирным текстом, ссылками и списками. Проблемы начинаются с вставкой из Word (приезжает тонна служебной разметки), загрузкой картинок на сервер, таблицами и одинаковым поведением во всех браузерах: execCommand в каждом из них генерирует немного разный HTML. Если требования выходят за базовое форматирование, разумнее взять готовый компонент, чем тратить недели на отладку.
Хороший компромисс для legacy-проекта: оставить самодельный редактор там, где он уже работает и пользователи им довольны, а на новых формах сразу подключать готовую библиотеку. Серверная часть при этом не меняется: она по-прежнему получает HTML из скрытого поля и пропускает его через тот же фильтр.
Как это делают сегодня
Вопросы и ответы
Почему при сохранении появляется ошибка «A potentially dangerous Request.Form value»?
Сработала встроенная проверка запроса ASP.NET на HTML в полях формы. Кодируйте текст перед отправкой или отключайте проверку точечно с обязательной фильтрацией на сервере.
Чем iframe лучше div с contenteditable?
iframe изолирует стили и скрипты страницы от редактируемого документа. contenteditable проще в подключении, его используют большинство современных редакторов.
Как вставлять картинки?
Команда insertImage вставляет изображение по URL. Сам файл нужно предварительно загрузить на сервер отдельным запросом и получить его адрес.