Деловая неделя

Одно тянет за собой другое, приходится учитывать кучу мелочей, из-за того что все наши сайты существуют в двух ипостасях (на интернет-сервере и в виде дистрибутива для Windows типа WAMP или Denwer).

ЕЖЕНЕДЕЛЬНАЯ РЕКЛАМНАЯ ГАЗЕТА ДЛЯ ПРЕДПРИЯТИЙ «Деловая неделя» (Иркутск)

«Автозапчасти» внутри 2 (PHP, Javascript)

Поиск

Как это выглядит: щёлкнул по слову «Дверь пятая» в левом списке, по слову «Accord» в правом, щёлкнул по кнопке «L» в центре – поля и заполнились; потом кнопку «Найти» – всё и нашлось. А если не нашлось – по кнопке «Х», чтобы очистить поля поиска. Тут нет ничего примечательного, кроме флажка «Искать сразу» и поля со значением "60" и надписью «Результатов поиска на странице». Это всё работает: можете написать вместо "60", например, "600", нажать "оК" и посмотреть, что получится. А потом поставить галочку у флажка «Искать сразу» и пощёлкать по словам списков.

Код javascript вроде бы должен быть простой – по щелчку на элементе вставить некоторое значение в поле формы. Нечто вроде:

document.getElementById("name").value=this.value

Он, однако, осложнаяется двумя моментами. 1) Элемент, из которого берётся текст для вставки, не INPUT, и у него нету свойства value. Поэтому значение извлекаем примерно так:

var val=this.firstChild.nodeValue

(мы, конечно, можем быть уверены, что firstChild у нашего элемента – нужный нам textNode, так как сами генерируем списки элементов).

2) Список элементов для выбора, и поле для вставки значения находятся в разных фреймах. Поэтому целевой элемент находится по формуле:

var target=parent.main.document.getElementById("name")

Ну, и на всякий случай он (и ряд других элементов) находится в отдельной функции initlistvar(), предусматривающей вероятность ошибки: вдруг фрейм main ещё не загрузился, а мы уже обращаемся к его объекту? Например, ставим значение «Результатов поиска на странице» равным 300 (чтоб main подольше грузился), потом быстро-быстро щёлкаем подряд по разным словам в левом списке. Если убрать обработку ошибки (try – catch), браузеры время от времени будут выдавать "object is null" или что-то подобное.

Всё вместе (функции insval(obj), initlistvar() и др.) можно посмотреть в файле http://irkutsk.ir2.ru/avto/edit.js.

Куки и WAMP

Обычно пользовательские настройки вида Результатов поиска на странице мы храним в базе данных на сервере. Что предполагает авторизацию пользователя. Для неавторизованных пользователей ничего, кроме куки, пожалуй, не придумаешь. По инерции мы всегда устанавливали куки так же, как записывали информацию в БД – через серверные скрипты (PHP). Потом (на сайте Автозапчастей) решили стать последовательными и сэкономить немного трафика (и времени) – устанавливая куки, как положено, через javascript (без участия сервера).

В нашем настольном справочнике (Ю.Лукач, однако, про куки сказано очень мало. Сами файлы куки для ИЕ выглядят внутри совсем не так, как описывает спецификация. Как они выглядят для FF или Оперы – вообще загадка. Ну, неважно, как они там выглядят, нам ведь нужно хоть как-то их туда записать. Тут вполне помогла статья Установить/получить cookie. Не совсем, правда, понятно, зачем там устанавливаемые значения кодируются функцией escape. Впрочем, нам это дважды всё равно, так как значения наших куки очень простые, состоят только из латинских букв и цифр: true, false, 60.

В связи с этой простотой, кстати, пришлось подумать о безопасности (проверяя значение с помощью isFinite): ведь последнее число (60) попадает в раздел limit sql-запроса, и если туда попадёт буква вместо цифры, сервер вернёт ошибку. В итоге получилось нечто вроде

function setcoo(name,val){
 if ("limit"==name) val=(isFinite(val))?val:60
 var expiresDate = new Date(); 
 expiresDate.setTime(expiresDate.getTime() + 36 * 24 * 60 * 60 * 1000)
 var expires = expiresDate.toGMTString()
 document.cookie = name + "=" + val + "; path=/"+hpref+"avto; expires=" + expires
}

На сервере, кстати, PHP тоже всё проверяет:

$lim=intval(@$COOKIE["limit"]);
$onecli=@$COOKIE["onecli"];
$onecli=("true"==$onecli)?$onecli:"";

– для переменной $lim принимаются только числовые значения, а $onecli может быть только "true" (или ничем).

Одно тянет за собой другое, приходится учитывать кучу мелочей, из-за того что все наши сайты существуют в двух ипостасях (на интернет-сервере и в виде дистрибутива для Windows типа WAMP или Denwer). Так, в частности, родилась на свет переменная hpref, обозначающая разницу в путях на сайте (irkutsk.ir2.ru/avto/) и в локальной версии (127.0.0.1/irkutsk.ir2.ru/avto/). Жирным в адресах выделены хосты, по оставшейся части видно, что на сайте надо устанавливать для куки путь /avto/, а в локальной версии – /irkutsk.ir2.ru/avto/. Переменная hpref равна в данном случае строке /irkutsk.ir2.ru на домашнем компьютере и пустой строке в Интернете.

Переменная hpref, в числе прочих, инициализируется и вычисляется в общем для всех сайтов конфигурационном файле configbase.php, локально он находится в корневой папке http-сервера 127.0.0.1, в Интернете – в корневой папке public_html учётной записи виртуального хостинга, в которую вложены папки всех сайтов.

Есть другой способ «примирения» путей на локальном сайте и в Интернете – он используется в дистрибутиве Денвер: там, во-первых, создаётся виртуальный диск с «реальными» путями a la unix (/usr/bin/perl), и на этот диск копируются все файлы сервера; во-вторых, всем сайтам даётся доступ через VirtualHost Apach'а, что, в свою очередь, требует дополнительных записей в файле c:\WINDOWS\system32\drivers\etc\hosts. Так что неизвестно ещё, что хуже. Логика нашего дистрибутива «Автозапчасти» – не оставлять следов на Windows нигде, кроме папки установки программы (ни в реестре, ни в файле hosts, ни в других местах). То есть стёр папку – удалил программу. Ну, правда, останутся ярлыки на Рабочем столе. Но это минимальное зло.

На самом деле Денвер, конечно, тоже не изменяет Windows, если завершать его работу корректно. Но даже временные записи в файле hosts создают коллизии. При отладке часто требуется смотреть результаты и локальной версии, и в сети. А если в файле hosts записано "127.0.0.1 irkutsk.ir2.ru", мы уже никак не сможем открыть интернет-сайт с данным именем. Более того, в Опере нами было замечено такое безобразие, как dns-кэширование, вследствие чего после изменений в файле hosts Опера продолжала использовать устаревшие настройки. То есть мы всё-таки думаем, что файл hosts не надо часто менять (особенно неявно для пользователя) без суровой необходимости.

Авторизация

Для редактирования (добавления) информации, нужна авторизация. Здесь она у нас самая простая, как на всём сайте irkutsk.ir2.ru, – "cookie-based" (с использованием PHP-сессии). Ну, не совсем, конечно, традиционная: если у пользователя нет учётной записи, она создаётся автоматически, с использованием введённых (новых) имени и пароля (нет отдельной формы для регистрации), и пользователь сразу авторизуется.

Испуганные многочисленными рассказами о хакерах, ворующих пароли из PHP-сессий (и даже из таблиц Mysql!), мы решили не хранить пароль в виде переменной сессии (зачем это нужно?), а поле с паролями в таблице Mysql зашифровать. Необратимо! ;-) Шутки шутками, а главная причина этого, конечно, – распространение сайта в виде локальной версии: в ней ведь всё как на хостинге, в том числе все таблицы базы данных.

Анализ правильности введённых логина и пароля производится таким sql-запросом: SELECT `id` FROM `firm` WHERE `user`='{$nme}' AND `pw`=OLD_PASSWORD('{$pwget}') – если он выдаёт запись из таблицы, значит, пользователь авторизован, если нет – пользователь отправляется на страницу восстановления пароля. Хотя слово «восстановление» тут не совсем подходит – пароль генерируется новый (и высылается на e-mail, указанный при регистрации). Коды скриптов авторизации и восстановления пароля довольно просты.

После успешной авторизации меняются все три фрейма: объявления в центральном фрейме отображаются только те, которые добавил данный пользователь; а словари наоборот увеличиваются в размерах – в них включается максимальное количество позиций, найденных нами в разных справочниках в Интернете (в неавторизованном режиме словари включают в себя только наименования, встречающиеся в нашей главной таблице запчастей). ИЕ и тут отличился своим неуправляемым кэшированием – пришлось для него отменить автоматическую перезагрузку фреймов (так как она не срабатывает) и оставить пользователю окно со ссылкой, по которой для начала редактирования нужно щёлкнуть.

Добавление объявлений

Мы решили сэкономить на форме добавления объявлений и использовать для этой цели ту же форму, что и для поиска. В авторизованном режиме можно так же, как в обычном, щёлкать по кнопкам и спискам (или заполнять поля вручную), но последствия будут разные – в зависимости от нажатой кнопки («Найти» или «Добавить»). При щелчке по «Найти» (или нажатии Enter) производится поиск; при щелчке по «Добавить» введённые в поля поиска значения добавляются в базу. Если, конечно, там было введено значение в поле «Наименование» (это единственное обязательное для заполнения поле).

Недостатки: 1) пользователю надо объяснять, что отдельной формы для добавления нет; 2) двоякое использование формы организовано не очень последовательно, поэтому браузеры гекко и Опера выдают предупреждающие ошибки (что-то вроде «У вас в форме есть элемент Файл, а метод используется GET»). Но пока у нас нет никаких идей насчёт исправления.

В принимающем информацию скрипте нет ничего интересного. За исключением, может быть, экранирования спецсимволов. Раньше мы всегда пользовались обычным addslashes и были довольны, пока не скачали себе скрипт статистики с сайта webew.ru и не обнаружили, что люди давно пользуются функцией mysql_real_escape_string(). Мы уже и раньше сталкивались с проблемой, например, символа возврата каретки в sql-запросах и вполне прониклись недостаточностью экранирования кавычек. Но устрашающе длинное название этого mysql_real... как-то нас насторожило. Шутка. На самом деле, эта функция нам не подошла, потому что может работать только после установления соединения с mysql-сервером, а мы никогда не устанавливаем соединение в явном виде (хотя тут ещё не до конца всё ясно, и mysql_real может оказаться так же неисчерпаем, как гламур).

В общем, наше решение – $val=addcslashes($val,"\"\'\r\n\0\x1a\t"); – кроме необходимого (рекомендованного mysql), в нём мы экранируем ещё и табуляцию. Да, а для поиска в базе данных ведь надо экранировать ещё и "_" (подчёркивание), а функция mysql_real_escape_string() не экранирует подчёркивание и % (процент). Поэтому при поиске список экранируемых символов удлиняется: "\"\'\r\n\0\x1a\t\_". А символ процента мы обрабатываем отдельно. То есть вообще не обрабатываем (не экранируем), предполагая, что он в норме не водится в наименованиях и спецификациях автозапчастей и что пользователь может добавить его только сознательно. Добавим для этого памятку на страницу (для %).

Скрипт приёма информации существует сам по себе, и к нему можно получить доступ, просто набрав его адрес в браузере. В этом случае (как справедливо пишут на webew), mod_rewrite является хорошей защитой. Но у нас пока нет на этом сайте mod_rewrite (ещё неясно даже, как будут дальше развиваться наши отношения с фреймами, какова будет общая структура доступа к страницам), и поэтому защита самая примитивная: во всех «критичных» скриптах проверяется как минимум авторизация. А она у нас, как мы говорили, состоит всего из одной серверной переменной – имени пользователя (ну, строго говоря, ещё из идентификатора пользователя, но это уже не принципиально). Вот мы и проверяем, существует ли переменная $SESSION["username"] с непустым значением.

Изменение и удаление существующих объявлений

Удалять можно, разумеется, только свои записи. Удаление производится простым запросом обращением к скрипту вида addzap.php?id=8385, а внутри скрипта sql-запрос (delete from `zap` where `kod`={$id} and `firmid`='$userid'), где как раз используется вторая серверная переменная – идентификатор пользователя (фирмы) $userid, поэтому если переменной нет (или она пуста), конец запроса будет выглядеть как `firmid`='0', а фирмы с нулевым идентификатором не существует.

Для пользователя возможность удаления реализована в виде ма-аленькой красной ссылки-крестика в начале каждой строки: удалять должно быть сложно.

Изменять (редактировать) существующие записи должно быть легко. Мы и постарались. Чтоб было интуитивно ясно, как в Acces'е: щёлкаешь по ячейке таблицы, и меняешь буквы. Начиналось всё с идеи «вообще-как-в-Access»: внутри каждой ячейки таблицы сделать INPUT – всё сразу доступно для редактирования. Начали мы, к счастью, внедрять идею с редактирования словарей – и сразу же вскрылась проблема: ни один браузер не может быстро нарисовать 1300 элементов INPUT; даже обычная, не динамическая html-страница с 1300 инпутами прорисовывается по 5 секунд в любом браузере.

Поэтому элемент INPUT генерируется javascript'ом, при щелчке по ячейке таблицы. Действие по событию click присваивается всем ячейкам (TD) редактируемой таблицы после загрузки страницы функцией initedit() (файл editmain.js). Тут есть некоторые сложности: как передать в sql-запрос информацию об имени поля и об идентификаторе строки? Идентификатор при создании кода таблицы присваивается родительскому элементу TR, а для имени поля мы не смогли придумать ничего оригинального: оно хранится в каждой ячейке, в атрибуте class элемента TD. Вероятно, можно как-то использовать элементы COL таблицы, но это нужно изучать.

После изменения информации в ячейке (по событию change) запись в базу данных производится в фоновом режиме, без перезагрузки страницы, по технологии, условно называемой нами Script: создаётся новый элемент SCRIPT, и его атрибуту src присваивается свойство savezap.php. Мы считаем, что эта технология вполне может заменить jshttprequest (более подробно говорили об этом в статье Аякс не нужен), на сегодня нам известен только один браузер, не поддерживающий данную технологию, – Konqueror. Ну, мы идём на этот риск – разозлить 0.00% пользователей (по статистике посещений), просматривающих наши сайты с помощью данного браузера.

В скрипте savezap.php, изменяющем существующую информацию в базе данных, проверяются две вещи: 1) авторизован ли пользователь (существует ли переменная сессии username); 2) свою ли строку пользователь хочет редактировать: update `zap` set `{$fld}`='{$val}' where `kod`={$id} and `firmid`='{$userid}', где $userid – соответствующая переменная сессии PHP.

Нельзя добавлять и удалять файлы (фотоизображения) у существующих записей. Это можно доделать позже примерно по такой схеме: 1) каждой строке добавляется ссылка «Редактировать»; 2) при щелчке по этой ссылке вся информация данной строки попадает в форму (вместе со скрытым полем – идентификатором строки); 3) кнопка «Добавить» превращается в «Сохранить изменения в строке 3852»; 4) принимающий скрипт – addzap.php – при наличии идентификатора строки использует sql-запрос update; 5) страница перезагружается.

Синхронизация локальной версии с интернет-сайтом

Это самая тёмная глава во всей истории сайта автозапчастей. Потому что мы, заранее не зная, как на практике будет использоваться эта возможность, решили сделать всё и сразу. Это, разумеется, привело к серьёзным логическим противоречиям, решение которых мы как раз и пытались здесь найти.

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

2) Но потом мы решили дать пользователю возможность ещё и скачивать с сайта себе на компьютер всё, чего нет в локальной версии. И вот это «всё, чего нет у меня» определить оказалось непросто. С записями, занесёнными от имени другой фирмы проблем нет. У каждой записи ведь есть идентификатор фирмы. Но как отличать записи, которые тот же самый пользователь занёс непосредственно на сайт, от записей, попавших туда выгрузкой из локальной версии?

Мы завели ещё поле идентификатора пользователя (или идентификатора сайта), зарезервировав для него в PHP-скриптах значения: 10 для Интерне-версии и >20 для локальной версии, и в главный ключ таблицы (primary key) включили три поля: kod, firmid, userid. После выгрузки записей на сайт, значение userid 20 в них меняется на 21, и второй раз уже эти записи выгрузить нельзя. Может быть, эта защита от повторных действий избыточна, так как в sql-запросах используется метод replace, и старые записи всё равно будут уничтожаться (не будут множиться). При редактировании старых записей в локальной версии значение userid 21 заменяется обратно на 20, и запись опять включается в число заданий для выгрузки.

При получении записей с сайта туда сначала отправляется максимальная дата «чужих записей» локальной версии. «Чужие записи» – это, во-первых, записи других фирм (с отличным от текущей авторизации firmid) и, во-вторых, записи с нашим firmid, но с userid<20. В итоге запрос выборки данных на сайте получился таким: select * from `zap` where `datupd`>'{$datupd}' and (`firmid`!='$userid' or `userid`<20). Нужно будет, конечно, дать ещё пользователю возможность через html-форму задавать произвольное значение максимальной даты ($datupd), как это сделано в нашей системе Инфодиск.

Данные между сайтами пересылаются в виде файла .sql.gz. Пересылкой занимается PHP с помощью функции fsockopen, что порождает проблему прокси-серверов (если пользователь подключается к Интернету, вводя в браузере и других программах данные для http-авторизации). Пока наши эксперименты с прокси-http-авторизацией через PHP завершились неудачей. На крайний случай, можно будет добавить резервную систему обмена – такую, как в Инфодиске: пользователь совершает обмен в два этапа: сначала выгружает данные в файл, затем отправляет файл на удалённый сервер через обычную html-форму (или наоборот, сначала скачивает файл с удалённого сервера, а потом загружает его в локальную версию через html-форму).

Про обмен данными sql между сайтами мы готовим отдельную статью, которая должна выйти в свет в ближайшую неделю.

© 2009, «Деловая неделя», Михаил Гутентог

Читать все комментарии (0)

Добавить комментарий:

*Автор:
E-Mail:
*Текст: