Изучаем PHP: Часть3. Аутентификация, работа с потоками данных, объекты и исключения

Из этого учебного пособия, последнего из трех в серии "Изучаем PHP", вы узнаете, как использовать PHP для создания простейшего приложения, поддерживающего workflow. В этой части рассматривается HTTP-аутентификация, доступ к документам посредством обработки потоков данных, создание объектов и обработка исключений.

Никлас Чейз, Независимый автор, Backstop Media

Никлас Чейз (Nicholas Chase) участвовал в разработке Web-сайтов для таких компаний, как Lucent Technologies, Sun Microsystems, Oracle и Tampa Bay Buccaneers. Ник успел побывать школьным учителем физики, редактором электронного научно-фантастического журнала, инженером в области мультимедиа, инструктором по Oracle и главным инженером в интерактивной коммуникационной компании. Он является автором нескольких книг, в том числе XML Primer Plus (Sams).



Тайлер Андерсон, Независимый автор, Stexar Corp.

Тайлер Андерсон (Tyler Anderson) прежде работал в DPMG.com, SEO-компании, для которой он писал программное обеспечение. Получил диплом по вычислительной технике в Brigham Young University в 2004 и степень магистра наук по вычислительной технике в декабре 2005, тоже в Brigham Young University. В настоящее время работает инженером в компании Stexar Corp., расположенной в Beaverton, Oregon. Вы можете связаться с ним по адресу tyleranderson5@yahoo.com.



16.01.2007

Прежде чем начать

Об этом учебном пособии

В этой, заключительной части учебного пособия мы закончим разработку учебного приложения, поддерживающего workflow, с которым мы работали в предыдущих двух частях. Мы добавим HTTP-аутентификацию, потоковое перемещение документов из каталогов, закрытых для доступа из сети, задействуем обработку исключений. Мы введем понятие объекта и изменим структуру нашего приложения, включив некоторые его части как объекты.

Кроме того, мы добавим для администратора системы возможность одобрять загруженные простыми пользователями документы и делать их общедоступными. Вот полный перечень стоящих перед нами задач:

  • Подключение и использование HTTP-аутентификации
  • Перемещение файла с использованием потока данных
  • Создание классов и объектов
  • Использование методов и свойств объектов
  • Описание и обработка исключений
  • Использование ID атрибутов XML
  • Организация проверки синтаксической корректности XML-документов с использованием DTD (Document Type Definition -- Опеределние Типа Документа)
  • Обеспечение контроля доступа к данным с использованием информации об источнике запроса

Для кого написано это пособие?

Это учебное пособие является заключительной частью в серии из трех пособий, описывающих создание с помощью PHP приложения, поддерживающего workflow. Мы предполагаем, что у читателя есть базовые знания по PHP и желание познакомиться с более сложными темами, такими как объектно-ориентированное программирование, HTTP-аутентификация, обработка потоков данных, классы, объекты, описание и обработка исключений, дополнительные возможности для работы с форматом XML.

Базовые знания по PHP, на которые мы опираемся в этом учебном пособии, включают знание синтаксиса, умение работать с такими структурами как HTML-формы и базы данных, знакомство с форматом файлов XML. Все эти темы обсуждались в Части 1 и Части 2 этой серии. Дополнительную информацию по теме этого пособия можно найти в разделе Ресурсы.

Необходимые условия

Вам необходимо иметь Web-сервер, PHP и базу данных. Если у вас есть доступ к сети, в которой установлен сервер PHP V5 с доступом к базе данных MySQL, то вы можете им воспользоваться. В противном случае загрузите и установите следующие пакеты:

HTTP-сервер -- Вне зависимости от того, работаете ли вы под Windows®, Linux®, UNIX® или Mac OS X, вы имеете возможность использовать Apache HTTP-сервер. Можно использовать различные версии, но примеры HTTP-аутентификации в этой части пособия ориентированны на версии Apache V2.X. Загрузить HTTP-сервер можно со страницы Apache. Если вы работаете под Windows®, вы можете также использовать Windows® IIS.

PHP -- Вам необходим дистрибутив PHP. Во время написания этого пособия в ходу были версии PHP V4 и V5, но в примерах этой части используются новые возможности версии V5. Загрузить дистрибутив можно со страницы PHP.

База данных -- одной из тем этого учебника является получение доступа к базе данных. Соответственно, вам нужна та или иная база данных. Мы разбираем доступ к базе MySQL, поскольку именно эта база данных как правило используется вместе с PHP. Загрузите базу данных со страницы http://dev.mysql.com/downloads/index.html.


Определим наши задачи и средства

Что уже было сделано

В двух первых частях этого пособия мы изучали язык PHP в процессе разработки простого приложения, поддерживающего workflow. Наше приложение дает пользователям возможность зарегистрироваться в системе, загрузить файлы на сервер и просмотреть загруженные файлы, а также и те файлы, которые были одобрены администратором системы. Итак, нами уже разработаны:

  • Страница регистрации новых пользователей. На этой странице с использованием HTML-формы пользователь системы может ввести регистрационное имя (оно должно быть уникальным), пароль и адрес электронной почты. Эти данные попадают на другую страницу нашей системы, проходят проверку, мы соединяемся с базой данных для того, чтобы убедиться в уникальности имени пользователя и записать в базу регистрационные данные.
  • Страница входа в систему для пользователей, которые уже имеют учетную запись. Имя пользователя и пароль проверяются с привлечением базы данных, если проверка прошла успешно, то для пользователя открывается сессия на сервере.
  • Простые элементы интерфейса. Возможности, которые предоставляет наш интерфейс, зависят от того, зарегистрировался ли пользователь в системе.
  • Страница загрузки, которая позволяет выбрать файл и отослать его на сервер из браузера. Служебная страница принимает файл и сохраняет его во временном каталоге на сервере, а также добавляет информацию о загруженном документе в XML-файл с использованием DOM (Document Object Model – Объектная модель документа).
  • Страница, на которой отображается информация о загруженных на сервер документах. Используя другой инструмент, SAX (Simple API for XML – Простой API для XML), мы написали функцию, которая читает информацию из XML-файла и отображает ее в форме таблицы на экране.

К концу второй части мы создали несложное, но работоспособное приложение, вы можете загрузить его тексты из "Изучаем PHP, Часть 2."

Каковы дальнейшие планы?

В заключительной части пособия мы планируем решить следующие задачи:

  • Добавить HTTP-аутентификацию, контролируемую Web-сервером. Мы сделаем доработки для нашей страницы регистрации, чтобы новые пользователи были зарегистрированы также и на Web-сервере.
  • К списку файлов, доступных на сервере, добавим ссылки, чтобы пользователь мог загрузить эти файлы. Для загрузки напишем новую функцию с использованием обработки потоков данных. Эта функция будет передавать браузеру документы из каталогов, к которым нет прямого доступа из сети.
  • Включить проверку адреса, с которого пришел запрос на загрузку документа. Для включения контроля загрузки будет использоваться тот факт, что перемещение файлов происходит посредством специальной программы, а не методом прямого доступа через HTTP-сервер.
  • Создать класс, который будет представлять документ, использовать объектно-ориентированные методы для доступа к файлу и его загрузки.
  • Добавить генерацию и обработку исключений для более точного решения проблем, которые могут возникнуть при работе приложения.
  • В XML-файл с описанием документов добавить информацию, которая позволит однозначно идентифицировать каждый файл. Использовать в форме для администратора checkbox'ы для отметки тех файлов, которые решено одобрить и открыть для публичного доступа.
  • Добавить в XML-файл ключевое поле для прямого доступа к элементам fileInfo и управления процессом приемки файлов.

Начнем с создания общей стартовой страницы для нашей системы..

Стартовая страница

До сих пор мы разрабатывали отдельные части нашей системы, теперь давайте сведем все воедино. Начнем с создания стартовой страницы, которая будет служить "посадочной полосой" для наших посетителей. Создайте файл с именем index.php и добавьте в него следующий код:

<?php

   include ("top.txt");
   include ("scripts.txt");

   display_files();

   include ("bottom.txt");

?>

Первый оператор include() загружает интерфейс начальной части страницы и открывает сессию, второй загружает написанные нами до сих пор скрипты. Среди этих скриптов присутствует вызов функции display_files(), определенной выше в "Изучаем PHP, Часть 2", эта функция выводит на экран список всех документов, загруженных текущим пользователем или одобренных администратором системы. Последний оператор include() содержит закрывающие теги для формирования законченной HTML-страницы.

Сохраните этот файл в том же каталоге, где хранятся созданные вами ранее файлы. Например, вы можете сохранить его в головном каталоге для документов вашего сервера, если вы при запуске HTTP-сервера укажете адрес http://localhost/index.php, то увидите страницу, подобную приведенной ниже на Рисунке 1.

Внешний вид это страницы незамысловат. Кроме того, если бы мы показали реальную страницу, то список фалов был бы пуст, поскольку, с одной стороны, пользователь еще не зарегистрирован и мы не можем видеть его собственных файлов, с другой стороны, у нас нет пока процедуры одобрения файлов и не должно быть файлов, одобренных администратором. Мы видим файлы потому, что искусственно приписали им признак approved.

Рисунок 1. Базовая стартовая страница
Базовая стартовая страница

На стартовой странице мы видим ссылки Register и Login, поскольку пользователь еще не зарегистрировался в системе. В следующем разделе мы разберем новый подход к процессу регистрации.


Использование HTTP-аутентификации

HTTP-аутентификация

До сих пор для регистрации в нашей системе пользователи вводили свой логин и пароль в HTML-форму и отсылали данные. Далее происходил контроль имени пользователя и пароля с использованием базы данных MySQL, если проверка проходила успешно, то для пользователя создавалась сессия на сервере и его имя записывалось в массив $_SESSION для использования в дальнейшем.

Эта процедура годилась для целей обучения, но в реальных системах такой подход может вызвать проблемы. Если наше приложение, поддерживающее workflow, является частью большой системы, то пользователь, возможно, уже прошел процедуру регистрации. С другой стороны, если пользователь зарегистрировался в нашей системе, то мы должны сохранить эту информацию для того, чтобы ему не пришлось заново вводить данные при входе в другие приложения. Системы, в которых пользователь вводит пароль один раз, называются Single Sign-On (Системы однократной регистрации).

Чтобы обеспечить однократную регистрацию, переложим функции контроля процесса входа в систему на HTTP-сервер. Получив запрос от браузера, сервер проверит, включает ли он имя пользователя и пароль, если не включает, то сервер выведет окно для их ввода. После того как пользователь введет подходящие регистрационные данные, они будут передаваться вместе с последующими запросами до тех пор, пока браузер не будет закрыт.

Прежде всего, мы должны включить возможность HTTP-аутентификации на нашем сервере.

Включение HTTP-аутентификации

Следует иметь в виду, что настройка механизма аутентификации существенно зависит от сервера. В этом разделе мы будем рассматривать настройку аутентификации для сервера Apache 2.X, если вы используете другой сервер, то обратитесь за указаниями к документации. Если у вас возникнут трудности с настройкой HTTP-сервера, то вы можете просто пропустить этот раздел, наша система будет корректно работать и со старым типом аутентификации.

Для того чтобы система HTTP-аутентификации могла работать, должно быть известно, какой уровень безопасности установлен для того или иного каталога. Установить права доступа для конкретного каталога можно, сделав соответствующие изменения в главном конфигурационном файле сервера. Есть и другой путь, можно использовать файл .htaccess, который содержит инструкции для того каталога, в котором он размещен.

Рассмотрим пример. Допустим, вы хотите, чтобы к вашим документам имели доступ только те пользователи, которые прошли регистрацию. В том каталоге, в котором вы храните свои файлы, создайте специальный подкаталог loggedin. Если основной каталог /usr/local/apache2/htdocs, то надо создать каталог /usr/local/apache2/htdocs/loggedin.

Теперь мы должны сообщить серверу, что мы хотим ввести для этого каталога особые правила доступа, для этого в файл httpd.conf добавим следующие строки:

<Directory /usr/local/apache2/htdocs/loggedin>
 AllowOverride AuthConfig
</Directory>

Разумеется, вы должны указать головной каталог в соответствии с настройками своего сервера.

Настройка аутентификации

Следующим шагом надо создать текстовый файл с именем .htaccess в каталоге loggedin и добавить в него следующие строки:

AuthName "Registered Users Only"
AuthType Basic
AuthUserFile /usr/local/apache2/password/.htpasswd
Require valid-user

Рассмотрим, что означают эти строки. За ключевым словом AuthName идет заголовок, который появится в окне для ввода имени пользователя и пароля. AuthType задает тип используемой аутентификации, в нашем случае тип будет Basic, то есть имя пользователя и пароль будут передаваться открытым текстом. AuthUserFile указывает на файл, в котором хранятся допустимые регистрационные данные. (Мы создадим этот файл чуть позже.) И наконец, директива Require определяет, кто именно может иметь доступ к этому каталогу. В нашем случае сказано, что доступ могут иметь все зарегистрированные пользователи, но вы можете также указать здесь группу или конкретного пользователя.

Перезапустите свой HTTP-сервер, для того чтобы актуализировать изменения в настройке.

(Для Apache V2.0 надо выполнить последовательно команды <APACHE_HOME>/bin/apachectl stop и <APACHE_HOME>/bin/apachectl start.)

Теперь осталось создать файл с регистрационными данными.

Создание файла с регистрационными данными

Итак, нам нужен файл, содержащий данные, к которым сервер будет обращаться для проверки логина и пароля. Ниже, в разделе Добавление новых пользователей, мы рассмотрим работу с этим файлом посредством PHP, пока же создадим и заполним файл .htpasswd непосредственно.

Сначала надо выбрать каталог, в котором будет храниться наш файл. Доступ извне к этому каталогу, естественно, должен быть закрыт. Но это должен быть такой каталог, с которым можно работать из PHP, иначе мы не сможем управлять ситуацией. Этим требованиям, например, удовлетворяют подкаталоги каталога apache2. Если вы выберете другой каталог, то внесите соответствующие изменения в файл .htaccess.

Для того чтобы создать файл с логином и паролем, выполните команду (подставив свой каталог и имя пользователя):

htpasswd -c /usr/local/apache2/password/.htpasswd roadnick

Вы получите приглашения на ввод пароля и повтор пароля:

htpasswd -c /usr/local/apache2/password/.htpasswd roadnick
New password:
Re-type new password:
Adding password for user roadnick

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

roadnick:IpoRzCGnsQv.Y

Заметьте, что пароль зашифрован. Следует иметь это в виду при добавлении данных из приложения.

Теперь посмотрим, как это работает.

Вход в систему

Чтобы увидеть, как это работает, нам надо иметь доступ к файлу в защищенном каталоге. Переместите файлы uploadfile.php и uploadfile_action.php в каталог loggedin, а файл index.php скопируйте туда же с именем display_files.php.

Во всех трех файлах надо внести изменения в операторах include(), чтобы обеспечить правильный путь к включаемым файлам:

<?php

   include ("../top.txt");
   include ("../scripts.txt");

   echo "Logged in user is ".$_SERVER['PHP_AUTH_USER'];

   display_files();

   include ("../bottom.txt");

?>

Откройте свой браузер, указав адрес http://localhost/loggedin/display_files.php, чтобы увидеть, как это работает. В момент вызова браузера имя пользователя и пароль не заданы, поэтому на экране появится окно для регистрации, которое можно видеть ниже, на Рисунке 2.

Рисунок 2. Окно для регистрации
Окно для регистрации

Если вы введете имя и пароль, которые вы задали выше, то увидите привычную страницу.

Использование регистрационных данных

Итак, мы видим привычную страницу, как на Рисунке 3, в верхней части которой есть сообщение о том, что пользователь зарегистрирован, и выведено имя пользователя. Однако прочее содержание страницы выглядит так, как если бы регистрация не произошла. Мы видим ссылки Register и Login, список файлов содержит только те, которые были уже одобрены администратором, но не содержат файлов, загруженных пользователем roadnick и имеющих состояние pending (то есть, ожидающих одобрения).

Рисунок 3. Пользователь зарегистрирован... почти
Пользователь зарегистрирован... почти

Проблема возникает потому, что имя пользователя хранится в элементе массива $_SERVER['PHP_AUTH_USER'], а не в $_SESSION["username"]. Конечно, мы можем пройти по тексту нашего приложения и сделать всюду замену одной переменной на другую, но это – масса работы и такой путь нас не привлекает.

Другой путь состоит в том, чтобы в начале сессии задать переменную $_SESSION["username"] на основе значения $_SERVER['PHP_AUTH_USER'] тогда приложение будет работать в точности так, как оно работало раньше. Давайте сделаем это в файле top.txt сразу после оператора открытия новой сессии или присоединения к уже существующей сессии:

<?
   session_start();
   if (isset($_SESSION["username"])){
      //Do nothing
   } elseif (isset($_SERVER['PHP_AUTH_USER'])) {
      $_SESSION["username"] = $_SERVER['PHP_AUTH_USER'];
   }
   

?>
<html>
<head>
<title>Workflow System</title>
</head>
<body>

Теперь единственный способ заставить браузер забыть имя пользователя –- это закрыть его. Однако, это не всегда удобно. Поэтому следует добавить к нашей системе опцию выхода, logout. (В рамках нашего пособия мы не будем разбирать процедуру выхода из системы, просто добавим опцию для полноты картины.)

Если имя пользователя не определено ни в массиве $_SESSION, ни в массиве $_SERVER, то это означает, что процедура регистрации не прошла и пользователь действительно не определен. Теперь, если пользователь зарегистрирован, мы увидим такую страницу, какая показана на Рисунке 4.

Рисунок 4. Пользователь зарегистрирован
Пользовател зарегистрирован

Изменения в интерфейсе

Прежде чем двигаться дальше, внесем небольшие изменения в файл top.txt. Заменим ссылку опции Login вместо старой страницы регистрации, login.php, используем новый файл, display_files.php, предназначенный для HTTP-аутентификации:

...
<tr>
   <td width="30%" valign="top">
      <h3>Navigation</h3>
 
<?php
   if (isset($_SESSION["username"]) || isset($username)){
?>
      <p>You are logged in as <?=$_SESSION["username"].$username?>. <!--
You can <a href="/logout.php">logout</a> to login as a different user.--></p>
 
      <p><a href="/loggedin/uploadfile.php">Upload a file</a></p>
      <p><a href="/loggedin/display_files.php">Display files</a></p>
 
<?php
   } else {
?>
      <p><a href="/registration.php">Register</a></p>
      <p><a href="/loggedin/display_files.php">Login</a></p>
<?php
   }
?>
   </td>

Итак, мы изменили ссылку для входа в систему и добавили новую опцию для отображения списка файлов. Кроме того, мы закомментировали ссылку на выход из системы, поскольку эта тема выходит за рамки нашего пособия.

Осталось только изменить процедуру регистрации нового пользователя.

Добавление новых пользователей

Итак, мы должны связать процедуру регистрации нового пользователя с файлом .htpasswd. Запись нового пользователя в файл надо сделать после того, как учетная запись включена в базу данных. Откройте файл registration_action.php и добавьте в него строки:

...
           $passwords = $_POST["pword"];
           $sql = "insert into users (username, email, password) values ('"
                                      .$_POST["name"]."', '".$_POST["email"]
                                      ."', '".$passwords[0]."')";
           $result = mysql_query($sql);

           if ($result){
              echo "It's entered!";

              $pwdfile = '/usr/local/apache2/password/.htpasswd';
              if (is_file($pwdfile)){
                 $opencode = "a";
              } else {
                 $opencode = "w";
              }
              $fp = fopen($pwdfile, $opencode);
              $pword_crypt = crypt($passwords[0]);
              fwrite($fp, $_POST['name'].":".$pword_crypt."\n");
              fclose($fp);

           } else {
               echo "There's been a problem: ".mysql_error();
           }
        } else {

           echo "There is already a user with that name.  Please try again. <br />";
...

Прежде чем вносить изменения, убедитесь, что у вас все в порядке с правами доступа. Если файл уже существует, то пользователь должен иметь право записи в него на вашем Web-сервере. Если файла нет, то должно быть право записи в соответствующий каталог.

Перед тем как открыть файл, мы проверили, существует ли он. Если файла нет, то мы открываем новый файл, а если он существует –- продолжаем добавлять строки в старый.

Как вы могли видеть выше, пароли хранятся в шифрованном виде, поэтому прежде чем записать строку с паролем в файл, мы должны зашифровать его, для этого используется функция crypt(). После записи регистрационных данных файл закрывается.

Проверьте, все ли в порядке. Для этого закройте браузер (имя пользователя и пароль будут очищены) и затем попробуйте открыть его с адресом http://localhost/index.php.

Нажмите ссылку Register и создайте новую учетную запись. Затем снова закройте браузер и попробуйте открыть защищенную страницу. Новый логин и пароль должны работать.


Потоки данных

Что такое потоки данных?

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

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

Способ доступа к файлам может быть различен в зависимости от того, каким образом они были сохранены. Доступ к файлу на локальном сервере отличается от доступа к удаленным ресурсам посредством HTTP или FTP.

Для обеспечения прозрачности доступа к файлам многие языки программирования, в том числе и PHP, имеют специальные оболочки потоков данных. Вместо того чтобы прямо пытаться открыть файл, вы обращаетесь к программе-оболочке, которая знает, каким способом можно получить к нему доступ.

Чтобы обратиться к оболочке, надо, прежде всего, знать, какие оболочки доступны в вашей системе. В PHP есть функция stream_get_wrappers(), которая возвращает массив всех доступных оболочек, вы можете распечатать этот массив:

<?php

print_r(stream_get_wrappers());

?>

Функция print_r() очень удобна для распечатки содержимого массива. Скорее всего, вы увидите следующий вывод:

Array
(
    [0] => php
    [1] => file
    [2] => http
    [3] => ftp
)

Таким образом, у вас есть простой путь сохранения файлов на удаленном Web-сервере и на FTP-сервере, при этом способ сохранения ничем не будет отличаться от способа сохранения файлов на локальном сервере, и весь код этого раздела будет работать. Единственное отличие будет в имени вызываемой оболочки.

Загрузка файлов с сервера

Для того чтобы пользователи могли загрузить файл с сервера, сервер прежде должен получить этот файл. Кроме самого файла, требуется еще и информация о нем, для того чтобы браузер мог корректно отображать этот файл. Для решения обеих этих задач создадим новый файл с именем download_file.php и поместим его в каталог loggedin. Добавьте в этот файл следующий код:

<?php

   include ("../scripts.txt");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];
   $filepath = UPLOADEDFILES.$filename;

   if($stream = fopen($filepath, "rb")){
      $file_contents = stream_get_contents($stream);
      header("Content-type: ".$filetype);
      print($file_contents);
   }

?>

Легко понять, что именно происходит в этом фрагменте кода. Сначала мы открываем файл для чтения и буферизации. По сути дела, функция fopen() создает ресурс, который содержит ссылки на файл. Затем этот ресурс передается функции stream_get_contents(), которая читает содержимое файла в отдельную строку.

Эту строку мы можем передать браузеру, но при этом могут возникнуть проблемы. Поскольку браузер не знает, в какой форме отобразить переданные данные, то он будет отображать их просто как текст. Хорошо, если файл был текстовый, а что если это была картинка или сложный HTML-файл? Поэтому, прежде чем передавать содержимое файла, мы передадим браузеру заголовок header, который будет содержать информацию о типе файла (Content-type), например, image/jpeg.

Наконец, мы передаем содержимое файла браузеру для вывода. Поскольку браузер уже получил заголовок, то он знает, как следует обработать переданный файл.

Для того чтобы посмотреть как это работает, просто добавьте имя и тип файла к адресу URL:

http://localhost/loggedin/download_file.php?file=timeone.jpg&filetype=image/jpeg

Откройте браузер с таким адресом (подберите вместо timeone.jpg подходящую картинку из своих запасов) и вы увидите результат, наподобие Рисунка 5.

Рисунок 5. Загрузка файла
Загрузка файла

Добавим ссылки на файлы

Поскольку вся информация, необходимая для страницы загрузки, может быть добавлена в URL, достаточно добавить ссылки в список файлов, чтобы пользователи могли их загружать. При выводе на экран списка файлов, доступных для загрузки, мы использовали технологию SAX, то есть наш экранный вывод создавался при помощи обработчика содержания, точнее, класса с именем Content_Handler. Добавим следующий код к нашему классу в файле scripts.txt:

class Content_Handler{

  private $available = false;
  private $submittedBy = "";
  private $status = "";

  private $currentElement = "";

  private $fileName = "";
  private $fileSize = "";
  private $fileType = "";

  function start_element($parser, $name, $attrs){
...
  }

  function end_element($parser, $name){

     if ($name == "workflow"){
        echo "</table>";
     }

     if ($name == "fileInfo"){
        echo "<tr><td><a href='download_file.php?file=".$this->fileName."
&filetype=".$this->fileType."'>"
                       .$this->fileName."</a></td>".
                 "<td>".$this->submittedBy."</td>".
                 "<td>".$this->fileSize."</td>".
                 "<td>".$this->status."</td></tr>";

        $this->fileName = "";
        $this->submittedBy = "";
        $this->fileSize = "";  
        $this->status = "";
        $this->fileType = "";

        $this->available = false;
     }

     $this->currentElement = "";

  } 

  function chars($parser, $chars){

     if ($this->available){
         if ($this->currentElement == "fileName"){
            $this->fileName = $this->fileName . $chars;
         }
         if ($this->currentElement == "fileType"){
            $this->fileType = $this->fileType . $chars;
         }
         if ($this->currentElement == "size"){
            $this->fileSize = $this->fileSize . $chars;
         }
      }

  } 
}

Во-первых, к той информации, которую мы собираем о каждом элементе fileInfo, мы добавили поле fileType, в которое и будет помещен тип файла.

Используя функцию chars(), мы сохраняем значение типа в нашу переменную и используем ее при выводе строки с информацией о файле. Обратите внимание на то, что имя файла теперь служит текстовым элементом ссылки в формате HTML на сам файл. Результат изменений на странице можно видеть на Рисунке 6.

Рисунок 6.Ссылки на файлы
.Ссылки на файлы

Попробуйте нажать на ссылку и проверить, работает ли она.

Теперь давайте выделим наш процесс в самодостаточную конструкцию, а именно, создадим объект.


Использование объектов

Что такое объект?

Итак, в этом разделе мы вплотную подошли к понятию объекта. Все, что мы делали до сих пор, было процедурным программированием. Вызывались функции, переменные передавались в них как параметры, отдельные действия были объединены в довольно объемную программу, которая выполнялась от начала до конца. Теперь мы постараемся уйти от этого.

Центральной концепцией объектно-ориентированного программирования является объединение свойств и функций некоторого предмета в самодостаточную конструкцию, которая и называется объектом. Рассмотрим в качестве примера электрический чайник. Он имеет свойства, такие как цвет и максимальная температура, и функции, а именно, нагревание воды и отключение при закипании.

Если мы захотим описать чайник как объект, то должны определить его свойства или переменные (пусть цвет будет представлен переменной color, а максимальная температура -- переменной maximumTemperature) и функции или методы, в нашем случае есть два метода: heatWater() -- нагреть воду и turnOff() -- отключиться при закипании. Если вы будете писать программу взаимодействия с электрическим чайником, то в ней будет просто вызван метод чайника heatWater(), при этом вам не нужно думать о том, какие произойдут физические действия, поскольку метод есть составляющая самого объекта.

Приблизим эту концепцию к нашим задачам, а именно, создадим объект, который будет представлять загружаемый файл. У него будут такие свойства как имя и тип файла, и методы, например, download() -- загрузка.

Надеемся, что вы сориентировались в объектно-ориентированном подходе, а сейчас нам предстоит подняться еще на один уровень абстракции. Дело в том, что для формального описания свойств и функций используется не объект, а класс объектов. Класс работает как образец или форма для создания объектов определенного типа. Об объектах говорят как об экземплярах данного класса.

Итак, перед нами стоит задача создать описание нашего объекта, создать класс.

Создание класса WFDocument

Для того чтобы работать с объектами, необходимо создать класс, содержащий описание переменных и методов. Мы могли бы поместить определение нашего класса в файл scripts.txt file, но, поскольку мы поставили перед собой задачу улучшить структуру нашего приложения, мы поместим его в отдельный файл, WFDocument.php, и запишем этот файл в основной каталог. Добавьте в этот файл следующий код:

<?php

include_once("scripts.txt");

class WFDocument {

   function download($filename, $filetype) {

      $filepath = UPLOADEDFILES.$filename;

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$filetype);
        print($file_contents);
      }
   }
}

?>

Поскольку нам необходима константа UPLOADEDFILES, первым шагом мы включили файл scripts.txt. Затем приступили к созданию класса. Наш класс, WFDocument будет иметь только один метод, download(), совпадающий с функцией, которая была описана в файле download_file.php, за одним исключением: имя файла и тип файла будут передаваться методу как параметры, а не извлекаться непосредственно из массива $_GET.

Теперь мы готовы создать экземпляр класса, объект.

Создание объекта типа WFDocument

В Части 2 этой серии мы уже занимались созданием объектов определенного класса, когда работали с DOM, но тогда мы не стали давать объяснений по этому поводу. Разберем теперь этот процесс подробнее.

Откройте файл download_file.php и внесите следующие изменения:

<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->download($filename, $filetype);

?>

Во-первых, мы заменили включаемый файл, вместо файла со скриптами мы включаем файл WFDocument.php, в котором содержится описание класса WFDocument. (Некоторые разработчики считают удобным собирать определение всех классов своей системы в один файл и включать этот файл, а некоторые -- создавать отдельный файл для каждого класса.)

Затем создается новый объект типа WFDocument, для этого используется ключевое слово new, для ссылок на этот объект будет использована переменная $wfdocument.

Получив ссылку на объект, мы можем вызывать его публичные методы, в нашем случае есть только один метод, download(), для вызова метода объекта используется оператор ->. Появление этого оператора можно интерпретировать как указание: "использовать метод (или свойство), принадлежащий данному объекту".

Сохраните файл и проверьте, как он работает, выбрав одну из ссылок на нашей странице. Результат будет точно такой же, каким был ранее, разница состоит не в действиях, а в способе вызова.

Создание свойств

Методы представляют собой только одну сторону объекта. Главная идея объекта состоит в том, что он содержит в себе всю информацию о себе самом. Для того чтобы наш объект был полноценным, он должен включать в себя в качестве свойств имя и тип файл, а не получать их как параметры метода download(). Включим в определение класса эти свойства:

<?php

include_once("../scripts.txt");

class WFDocument {

   public $filename;
   public $filetype;

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$this->filetype);
        print($file_contents);
      }
   }
}

?>

Обратите внимание на то, что мы объявили переменные вне функции, они являются частью класса, а не функции. Кроме того, мы объявили эти переменные с ключевым словом public, это означает, что доступ к ним возможен не только внутри класса, но и извне. Переменные могут быть также описаны как private, тогда доступ к ним будет возможен только изнутри класса, или как protected, тогда их можно использовать внутри данного класса и всех классах, которые являются его расширением. (Понятия расширения и наследования будут рассматриваться ниже, а разделе Описание пользовательских исключений, если вы раньше не сталкивались с ними, то вам придется немного подождать.)

Для того чтобы обращаться к свойству объекта, надо знать, какому объекту свойство принадлежит. Если обращение происходит извне, то мы указываем имя объекта, а если изнутри, то можно использовать ключевое слово $this, которое указывает на то, что свойство принадлежит данному объекту. Таким образом, конструкция $this->filename ссылается на свойство filename того объекта, в котором содержится этот код.

Теперь разберем вопрос, как приписать значения свойствам объекта.

Значения свойств объектов

Итак, вместо того чтобы передавать значения в качестве параметров, мы хотим определить их как свойства самого объекта. Для это внесем следующие изменения:

<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->filename = $filename;
   $wfdocument->filetype = $filetype;
   $wfdocument->download();

?>

Итак, для того чтобы присвоить значение свойству объекта, мы создаем этот объект и затем ссылаемся на его свойство, используя его имя, $wfdocument, оператор -> и имя свойства, обратите внимание на то, что знак $ перед именем свойства не ставится. После того как значение свойства задано, мы можем использовать его внутри объекта, поэтому мы вызываем метод нашего объекта download() без аргументов.

Существует и другой способ определения свойств объекта, мы рассмотрим его ниже.

Сокрытие свойств

Задать значения свойств класса напрямую вполне возможно, но это не лучший метод. Для повышения безопасности и упрощения модификации кода свойства принято скрывать внутри класса (такая техника известна как инкапсуляция). Для присваивания значений скрытым от публики свойствам будут использованы специальные функции, getters и setters, давайте рассмотрим пример такого подхода:

<?php

include_once("../scripts.txt");

class WFDocument {

   private $filename;
   private $filetype;

   function setFilename($newFilename){
      $this->filename = $newFilename;
   }
   function getFilename(){
      return $this->filename;
   }

   function setFiletype($newFiletype){
      $this->filetype = $newFiletype;
   }
   function getFiletype(){
      return $this->filetype;
   }

   function download() {

      $filepath = UPLOADEDFILES.$this-> getFilename()

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$this->getFiletype())
        print($file_contents);
      }
   }
}

?>

Прежде всего, мы снабдили наши свойства модификатором private, это означает, то они будут доступны только внутри того класса, в котором определены. Тем не менее, нам необходимо присвоить нашим свойствам значения и затем использовать эти значения, поэтому мы описали четыре метода: getFilename(), setFilename(), getFiletype() и setFiletype(). Обратите внимание, что использование методов внутри функции download() ничем не отличается от использования свойств.

Использование специальных функций для определения и извлечения значений удобно еще и тем, что дает больший контроль над данными. Вы можете, например, организовать проверку данных, прежде чем приписать их значения свойствам объекта.

Обращение к скрытым свойствам

После того как мы сделали свои свойства скрытыми, нам необходимо внести изменения в файл download_file.php:

<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->setFilename($filename);
   $wfdocument->setFiletype($filetype);
   $wfdocument->download();

?>

Теперь мы, наконец, вплотную подошли к стандартному способу задания свойств, к использованию конструктора.

Создание конструктора

Фактически, когда мы создаем новый объект любого класса, то вызываем его базовый конструктор. Но мы можем написать и свой конструктор, который будет вызываться каждый раз при создании нового экземпляра нашего класса. Для начала создадим совсем простой конструктор:

...
   function getFiletype(){
      return $this->filetype;
   }

   function __construct(){
      echo "Creating new WFDocument";
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;
...

Если вы запустите этот скрипт, то результат будет не слишком хороший, это можно видеть на Рисунке 7. Ошибка возникла потому, что скрипт пытается вывести на экран текст (Creating new WFDocument) до того, как выведен заголовок страницы, header.

Рисунок 7. Ошибка при запуске скрипта
Ошибка при запуске скрипта

Мы видим, что наш конструктор отработал, хотя мы явно и не вызывали метод __construct(), он был вызван в момент создания нового объекта. Теперь давайте используем конструктор для передачи объекту полезной информации.

Присваивание значений при создании объекта

Конструкторы используются чаще всего именно для задания начальных значений свойств объекта. Добавим в наш класс WFDocument конструктор, который будет задавать значения свойств filename и filetype при создании нового экземпляра этого класса:

...
   function getFiletype(){
      return $this->filetype;
   }

   function __construct($filename = "", $filetype = ""){
      $this->setFilename($filename);
      $this->setFiletype($filetype);
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

...

Когда создается новый объект, PHP прежде всего смотрит, определен ли для него конструктор, и выполняет инструкции, содержащиеся в конструкторе, если он определен. В нашем случае инструкции состоят в том, чтобы присвоить свойствам нового объекта значения переменных $filename и $filetype. Если эти значения не определены в момент вызова конструктора, то ошибки при выполнении программы не возникнет, так как при описании конструктора мы задали значение для этих переменных "по умолчанию", а именно, пустую строку.

Но как вызывается функция __construct()?

Создание объекта: вызов конструктора

Нет необходимости вызывать конструктор непосредственно, поскольку он неявно вызывается при создании любого нового объекта. Таким образом, именно в момент создания нового экземпляра класса $ WFDocument мы и должны передать наши начальные значения:

<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument($filename, $filetype);
   $wfdocument->download();

?>

Любая информация, переданная в момент создания экземпляра класса, попадает в конструктор. Таким способом мы создаем новый объект, который содержит все необходимые данные для загрузки файла.


Обработка исключений

Встроенные исключения

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

Начнем с самых простых исключений, точнее, с использования встроенного в PHP класса исключений Exception. Добавим в описание нашего класса, WFDocument, следующий код:

<?php

include_once("../scripts.txt");

class WFDocument {
...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } 
         } else {
           throw new Exception ("File '".$filepath."' does not exist.");
         }

      } catch (Exception $e) {

         echo "<p style='color: red'>".$e->getMessage()."</p>";

      }
   }
}

?>

Прежде всего заметим, что исключения не происходят сами по себе, они генерируются (или возбуждаются). Для генерации исключения используется ключевое слово throw (основное его значение, "бросать", редко используется в документации на русском языке, он англичане бросают-ловят свои исключения). Генерация исключения должна быть включена в блок try, а возникшее исключение затем перехватывается и его обработка его происходит в одном из блоков catch. В блок try помещается весь код, который должен быть выполнен. Если что-то идет не так, в нашем случае -- не существует файла с указанным именем, то в этом блоке генерируется исключение и PHP переходит к выполнению блока обработки исключения, catch.

Исключение обладает известным набором свойств, среди них номер строки и имя файла, в которых сгенерировано исключение, и сообщение. Как правило, текст сообщения задается при генерации исключения, как вы могли видеть выше. Для получения сообщения в блоке обработки исключения используется метод getMessage() самого исключения $e. Например, если вы попытаетесь загрузить с сервера файл, который не существует, вы увидите сообщение, выделенное красным цветом на Рисунке 8.

Рисунок 8. Результат обработки базового исключения
Базовое исключение

Реальная мощь механизма исключений проявляется тогда, когда вы используете свои собственные исключения.

Создание пользовательских исключений

В предыдущем разделе при изучении объектов мы отложили в сторону одно очень важное понятие, связанное с объектами, наследование. Давайте вернемся к этому сейчас.

Важнейшим преимуществом использования классов является возможность создать новый класс на базе уже существующего. В этом случае говорят, что новый класс является расширением или потомком старого. Давайте создадим новый класс исключений, NoFileExistsException, который будет расширением базового класса Exception:

class NoFileExistsException extends Exception {

   public function informativeMessage(){
      $message = "The file, '".$this->getMessage()."', called on line ".
           $this->getLine()." of ".$this->getFile().", does not exist.";
      return $message;
   }

}

(Для простоты мы включили код нового класса в файл WFDocument.php, но он может быть расположен и в другом месте.)

Итак, описание нового класса, NoFileExistsException, содержит в себе только один метод: informativeMessage(). Но, поскольку этот класс является расширением класса Exception, все все публичные методы и свойства объекта Exception также доступны и для объектов нашего класса.

Например, вы могли заметить, что внутри функции informativeMessage() вызываются методы getLine() и getFile(), хотя они тут и не определены. Это возможно потому, что они определены в базовом классе, Exception.

Теперь перейдем к обработке исключений.

Обработка пользовательских исключений

Генерация пользовательского исключения ничем не отличается от генерации базового:

function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } 
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (NoFileExistsException $e) {

         echo "<p style='color: red'>".$e->informativeMessage()."</p>";

      }
   }

Обратите внимание на то, что при генерации исключения мы передали только переменную $filepath , но несмотря на это, получили полное сообщение об ошибке, как можно видеть ниже на Рисунке 9.

Рисунок 9. Пользовательское исключение
Пользовательское исключение

Обработка нескольких исключений

Пользовательские классы исключений нужны для того, чтобы иметь возможность точнее распознавать ошибочную ситуацию. Часто в одном фрагменте кода генерируется несколько видов исключений, тогда мы будем иметь один блок try и несколько блоков catch. Давайте рассмотрим пример:

...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (NoFileExistsException $e) {

         echo "<p style='color: red'>".$e->informativeMessage()."</p>";

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
      }
   }
}

В этом примере мы сначала проверяем наличие файла, если файла нет, то возбуждаем исключение NoFileExistsException, иначе пытаемся открыть файл. При открытии существующего файла также может возникнуть ошибка, в этом случае мы сгенерируем базовое исключение. PHP определит тип исключения и перейдет к выполнению соответствующего блока catch.

Возможно, вам показалось, что генерация и обработка исключений -- слишком громоздкий механизм для вывода сообщений об ошибочных ситуациях, но никто не говорит, что обработчик может только вывести сообщение. Блок catch может включать в себя сколь угодно сложные действия, зависящие от ситуации, например, вы можете отослать извещение администратору системы.

У исключений есть еще одно удобное свойство. Вы можете определить большое число исключений, но вы не обязаны для каждого из них писать отдельный блок catch. Исключения могут распространяться, о том как это происходит, мы поговорим в следующем разделе.

Распространение исключений

Способность исключений распространяться тесно связана с механизмом наследования классов. Дело в том, что всякий объект, который является экземпляром класса-потомка, может трактоваться также и как экземпляр базового класса. Например, вы можете сгенерировать исключение NoFileExistsException, а перехватить его как общее, Exception:

...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
      }
   }
}

Если возбуждено исключение, то PHP двигается вниз по списку блоков catch до тех пор, пока не встретит первый, способный обработать данный тип исключение. В нашем случае блок только один и он обработает наше исключение, как показано ниже на Рисунке 10. Дело в том, что все исключения являются потомками встроенного класса Exception, и поэтому могут рассматриваться как его экземпляры.

Рисунок 10 Распространение исключений
>Распространение исключений

Собираем все вместе

Что нам осталось сделать

Итак, мы завершили подготовительные операции, теперь осталось только собрать все вместе и придать нашему приложению законченный вид. В этом разделе перед нами стоят следующие задачи:

  • Приписать каждому файл уникальный идентификатор
  • Выделить среди пользователей администраторов системы
  • Создать форму, которая позволит администратору выполнять процедуру одобрения файлов
  • Включить проверку адреса, с которого делается попытка загрузить файл

Начнем с создания класса Counter для хранения уникального номера файла.

Идентификация документов

В уникальном идентификаторе документа до сих пор не было необходимости, конкретный файл выбирался только при загрузке файлов с сервера, но тогда достаточно было явно указать его имя. Теперь нам придется подумать об идентификации документов, поскольку в процедуре одобрения администратором системы файлов, загруженных пользователями, нужен простой способ ссылаться на конкретный документ.

Для этого мы создадим новый класс, Counter, при помощи которого будет генерироваться уникальный ключ для каждого файла. Затем мы добавим новое ключевое поле в XML-файл, который хранит информацию о документах, и это позволит нам непосредственно обращаться к нужному элементу fileInfo. Сначала создадим определение нового класса, Counter. Вы можете поместить его, например, в файл scripts.txt:

class Counter{

   function getNextId(){
     $filename = "/usr/local/apache2/htdocs/counter.txt";
     $handle = fopen($filename, "r+");
     $contents = fread($handle, filesize($filename));

     $nextid = $contents + 1;
     echo $nextid;
     rewind($handle);
     fwrite($handle, $nextid);
     fclose($handle);

     return $nextid;
   }

}

У этого класса есть один метод, getNextId(), который открывает уже существующий файл, counter.txt, читает его содержимое, которое должно быть числом, и увеличивает это число на 1, именно это, увеличенное, значение и будет возвращаться методом. Затем указатель перемещается в начало файла, новое значение записывается на место старого и файл закрывается. Прежде чем начинать работать с этим классом, нужно создать указанный файл и записать в него значение равное 0.

Теперь мы готовы добавить ключевое поле в XML-файл.

Ключевое поле в XML-файле

Как вы помните, в "Изучаем PHP, Часть 2" мы создали XML-файл с информацией о загруженных на сервер документах. Теперь мы хотим добавить к элементу fileInfo атрибут ID, который будет ключевым полем и позволит быстро и однозначно находить в XML-файле нужный элемент. Включите в код функции save_document_info() следующие строки:

function save_document_info($fileInfo){

   $xmlfile = UPLOADEDFILES."docinfo.xml";
...
   $filename = $fileInfo['name'];
   $filetype = $fileInfo['type'];
   $filesize = $fileInfo['size'];

   $fileInfo = $doc->createElement("fileInfo");

   $counter = new Counter();
   $fileInfo->setAttribute("id", "_".$counter->getNextId());

   $fileInfo->setAttribute("status", "pending");

   $fileInfo->setAttribute("submittedBy", getUsername());
...
   $doc->save($xmlfile);

}

При загрузке нового документа на сервер и записи информации о нем в XML-файл будет создан новый объект типа Counter, вызван его метод getNextId() и полученный уникальный номер записан в атрибут id элемента fileInfo. Знак подчеркивания перед номером поставлен потому, что этот элемент будет иметь тип ID, значение которого не может начинаться с цифры.

В результате в XML-файл будет записана следующая информация (для наглядности в текст добавлены пробелы):

<fileInfo id="_13" status="pending" submittedBy="roadnick">
   <approvedBy/>
   <fileName>timeone.jpg</fileName>
   <location>/var/www/hidden/</location>
   <fileType>image/jpeg</fileType>
   <size>2020</size>
</fileInfo>

Внесенное нами изменение будет работать только для вновь загружаемых файлов, поэтому вам придется либо добавить атрибут id к уже существующим элементам fileInfo вручную, либо удалить файл docinfo.xml и загрузить все нужные для работы файлы на сервер заново.

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

Администраторы системы

Когда мы создавали таблицу пользователей в базе данных, то не приняли во внимание тот факт, что нам потребуется в дальнейшем разделять пользователей по их функциям в системе. Давайте позаботимся об этом. Зарегистрируйтесь в MySQL и выполните следующие команды:

alter table users add status varchar(10) default 'USER';
update users set status = 'USER';
update users set status = 'ADMIN' where id=3;

Первая команда добавляет новое поле, status, к таблице пользователей. Для вновь создаваемых пользователей системы это поле будет заполняться «по умолчанию» значением USER . Вторая команда присваивает всем уже зарегистрированным в системе пользователям статус USER, а третья изменяет это значение на ADMIN для одного конкретного пользователя (подберите разумный id для администратора в своей системе).

Теперь напишем функцию, которая возвращает статус текущего пользователя:

function getUserStatus(){
   $username = $_SESSION["username"];
   db_connect();
   $sql = "select * from users where username='".$username."'";

   $result = mysql_query($sql);
   $row = mysql_fetch_array($result);

   $status = "";

   if ($row) {
     $status = $row["status"];
   } else {
     $status = "NONE";
   }

   mysql_close();

   return $status;

}

Имя текущего пользователя мы берем из массива $_SESSION, затем устанавливаем соединение с базой данных при помощи функции, которую мы написали в первой части нашего пособия, db_connect(). Далее мы формируем строку с SQL-запросом и выполняем этот запрос.

Если в результате запроса была получена строка данных (в нашем случае может быть только одна строка), то переменной $status присваивается значение статуса из строки таблицы, в противном случае мы присваиваем ей значение NONE. Затем соединение с базой закрывается и функция возвращает значение статуса.

Поместите эту функцию в файл scripts.txt, чтобы она была доступна при работе со списком файлов.

Процедура одобрения файлов: форма

Сделаем дополнения в нашу форму. Если текущий пользователь является администратором системы, ему надо дать возможность изменить статус документа. Для этого внесем следующие изменения в класс Content_Handler:

class Content_Handler{

  private $available = false;
  private $submittedBy = "";
  private $status = "";

  private $currentElement = "";

  private $fileId = "";
  private $fileName = "";
  private $fileSize = "";
  private $fileType = "";

  private $userStatus = "";

  function start_element($parser, $name, $attrs){


     if ($name == "workflow"){

        $this->userStatus = getUserStatus($_SESSION["username"]);

        if ($this->userStatus == "ADMIN"){
           echo "<form action='approve_action.php' method='POST'>";
        }

        echo "<h3>Available files</h3>";
        echo "<table width='100%' border='0'><tr>".
                "<th>File Name</th><th>Submitted By</th>".
                "<th>Size</th><th>Status</th>";
        if ($this->userStatus == "ADMIN"){
            echo "<th>Approve</th>";
        }
        echo "</tr>";
     }

     if ($name == "fileInfo"){
        if ($attrs['status'] == "approved" || 
                  $attrs['submittedBy'] == $this->username){
           $this->available = true;
        }
        if ($this->available){
           $this->submittedBy = $attrs['submittedBy'];
           $this->status = $attrs['status'];
           $this->fileId = $attrs['id'];
        }
     }

     $this->currentElement = $name;

  }

  function end_element($parser, $name){

     if ($name == "workflow"){
        echo "</table>";

        if ($this->userStatus == "ADMIN"){

           echo "<input type='submit' value='Approve Checked Files' />";

           echo "</form>";
        }

     }

     if ($name == "fileInfo"){
        echo "<tr>";
        echo "<td><a href='download_file.php?file=".
                       $this->fileName."&filetype=".
                       $this->fileType."'>".
                            $this->fileName."</a></td>".
                 "<td>".$this->submittedBy."</td>".
                 "<td>".$this->fileSize."</td>".
                 "<td>".$this->status."</td><td>";

        if ($this->userStatus == "ADMIN"){
           if ($this->status == "pending") {
              echo "<input type='checkbox' name='toapprove[]' value='".
                         $this->fileId."' checked='checked' />";
           }
        }           

        echo "</td></tr>";

        $this->fileId = "";
        $this->fileName = "";
        $this->submittedBy = "";
        $this->fileSize = "";  
        $this->status = "";
        $this->fileType = "";

        $this->available = false;
     }

     $this->currentElement = "";

  } 

  function chars($parser, $chars){
...
  } 
}

Итак, мы определили два новых свойства: $fileId и $userStatus. Статус пользователя определяется один раз, при открытии головного элемента документа, workflow. Если пользователь является администратором, то для него добавляется элемент form, который содержит ссылку на соответствующую страницу, и столбец таблицы с заголовком Approve.

Форма, которая была открыта при обнаружении парсером начала документа, закрывается при обнаружении конца элемента workflow.

checkbox выводится в последнем столбце таблицы для тех документов, статус которых pending, поскольку у нас может быть несколько входов с одним и тем же именем, то поле представлено массивом toapprove[].

Результат нашей работы с дополнительной кнопкой и checkbox'ами можно видеть ниже на Рисунке 11.

Рисунок 11. Форма для одобрения файлов
Форма для одобрения файлов

Назначение ID

Итак, мы добавили в наш XML-файл атрибут id для элемента fileInfo, теперь мы должны сделать еще один шаг. В отличие от документов в формате HTML, в XML-документах недостаточно просто назвать атрибут "id" для того, чтобы он начал работать как ключ. Мы должны сопоставить документу некоторую схему, которая будет определять свойства нашего атрибута (обратите внимание, речь не идет о XML Schema -- стандарте описания структуры XML документа). Для этой цели мы будем использовать формат DTD (Document Type Definition – Определение типа документа). Прежде всего, добавим в наш документ ссылку на файл DTD:

function save_document_info($fileInfo){

   $xmlfile = UPLOADEDFILES."docinfo.xml";

   if(is_file($xmlfile)){
      $doc = DOMDocument::load($xmlfile);
      $workflowElements = $doc->getElementsByTagName("workflow");
      $root = $workflowElements->item(0);

      $statistics = $root->getElementsByTagName("statistics")->item(0);
      $total = $statistics->getAttribute("total");
      $statistics->setAttribute("total", $total + 1);

   } else{

      $domImp = new DOMImplementation;
      $dtd = $domImp->createDocumentType('workflow', '', 'workflow.dtd');

      $doc = $domImp->createDocument("", "", $dtd);

      $root = $doc->createElement('workflow');
      $doc->appendChild($root);

      $statistics = $doc->createElement("statistics");
      $statistics->setAttribute("total", "1");
      $statistics->setAttribute("approved", "0");
      $root->appendChild($statistics);
   }
...
}

Вместо того чтобы создавать документ прямо как экземпляр класса DOMDocument, мы создаем сначала объект DOMImplementation, затем используем его метод DcreateDocumentType() для создания объекта типа DTD. Наконец, создаем новый документ, используя созданную ранее схему DTD.

Если вы удалите старый файл docinfo.xml и загрузите на сервер новый документ, то в файле docinfo.xml появится следующая информация:

<?xml version="1.0"?>
<!DOCTYPE workflow SYSTEM "workflow.dtd">
<workflow><statistics total="3" approved="0"/>
...

Теперь мы должны создать файл схемы workflow.dtd.

Схемы DTD

Описание технологии работы со схемами для формата XML-выходит за рамки данного пособия, мы ограничимся тем, что просто создадим файл DTD, в котором будет определена структура файла docinfo.xml. Итак, откройте новый файл с именем workflow.dtd и сохраните его в том же каталоге, что и docinfo.xml. Добавьте в него следующий текст:

<!ELEMENT workflow (statistics, fileInfo*) >
<!ELEMENT statistics EMPTY>
<!ATTLIST statistics total CDATA #IMPLIED
                     approved CDATA #IMPLIED >
<!ELEMENT fileInfo (approvedBy, fileName, location, fileType, size)>
<!ATTLIST fileInfo id ID #IMPLIED>
<!ATTLIST fileInfo status CDATA #IMPLIED>
<!ATTLIST fileInfo submittedBy CDATA #IMPLIED>
<!ELEMENT approvedBy (#PCDATA)>
<!ELEMENT fileName (#PCDATA)>
<!ELEMENT location (#PCDATA)>
<!ELEMENT fileType (#PCDATA)>
<!ELEMENT size (#PCDATA)>

Строки файла DTD содержат описание элементов нашего документа и содержимое каждого элемента. Элемент workflow должен иметь в точности одного элемента-потомка типа statistics и некоторое (возможно, нулевое) число элементов-потомков типа fileInfo.

Сам элемент statistics описан как пустой, но имеющий два необязательных атрибута: total и approved, оба они имеют тип "строка".

Следующим идет описание элемента fileInfo, его атрибут id и будет необходимым нам ключом для поиска нужного элемента, поэтому его тип определен как ID.

Процедура одобрения файлов: запись данных в XML-файл

Страница для обработки значений из checkbox, approve_action.php, на которую ссылается наша форма, будет очень простой:

<?php

  include "../scripts.txt";

  $allApprovals = $_POST["toapprove"];
  foreach ($allApprovals as $thisFileId) {
     approveFile($thisFileId);
  }
  echo "Files approved.";

?>

Для каждого checkbox'а просто вызывается функция approveFile() поместим эту функцию в файл scripts.txt:

function approveFile($fileId){

   $xmlfile = UPLOADEDFILES."docinfo.xml";

   $doc = new DOMDocument();
   $doc->validateOnParse = true;
   $doc->load($xmlfile);

   $statisticsElements = $doc->getElementsByTagName("statistics");
   $statistics = $statisticsElements->item(0);
   
   $approved = $statistics->getAttribute("approved");
   $statistics->setAttribute("approved", $approved+1);
   
   $thisFile = $doc->getElementById($fileId);
   $thisFile->setAttribute("status", "approved");

   $approvedByElements = $thisFile->getElementsByTagName("approvedBy");
   $approvedByElement = $approvedByElements->item(0);
   $approvedByElement->appendChild($doc->createTextNode($_SESSION["username"]));

   $doc->save($xmlfile);

}

Прежде чем загрузить документ, мы устанавливаем значения свойства validateOnParse равным TRUE, это служит указанием парсеру, что документ должен быть проверен на соответствие схеме DTD. Благодаря этому атрибут id распознается как ключевой. После того как файл загружен, мы получаем ссылку на элемент statistics, соответственно, мы можем изменять значение его атрибутов при выполнении процедуры одобрения новых файлов. Вследствие того, что атрибут id описан как ключ, то есть, имеет тип ID, мы можем использовать метод getElementById() для доступа к конкретному элементу fileInfo и изменения его атрибута status.

И, наконец, нам нужна ссылка на элемент-потомок approvedBy текущего элемента, получив эту ссылку, мы добавляем новый узел типа Text и помещаем в него имя администратора, принявшего данный файл.

Последним оператором мы сохраняем наш XML-файл.

Обратите внимание, мы открывали и сохраняли XML-файл отдельно для каждого документа, в промышленных приложениях так не делают. Эффективнее было бы открыть файл один раз, внести в него все изменения и затем сохранить файл.

Проверка адреса при загрузке

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

Организуем контроль путем создания нового исключения. Добавим код в файл WFDocument.php:

<?php

   include_once("../scripts.txt");

class NoFileExistsException extends Exception {

   public function informativeMessage(){
      $message = "The file, '".$this->getMessage()."', called on line ".
           $this->getLine()." of ".$this->getFile().", does not exist.";
      return $message;
   }

}

class ImproperRequestException extends Exception {

   public function logDownloadAttempt(){
      //Additional code here
      echo "Notifying administrator ...";
   }

}

class WFDocument {

   private $filename;
   private $filetype;

   function setFilename($newFilename){
      $this->filename = $newFilename;
   }
   function getFilename(){
      return $this->filename;
   }

   function setFiletype($newFiletype){
      $this->filetype = $newFiletype;
   }
   function getFiletype(){
      return $this->filetype;
   }

   function __construct($filename = "", $filetype = ""){
      $this->setFilename($filename);
      $this->setFiletype($filetype);
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         $referer = $_SERVER['HTTP_REFERER'];
         $noprotocol = substr($referer, 7, strlen($referer));
         $host = substr($noprotocol, 0, strpos($noprotocol, "/"));
         if ( $host != 'boxersrevenge' &&
                                $host != 'localhost'){
            throw new ImproperRequestException("Remote access not allowed.
                        Files must be accessed from the intranet.");
         }

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }
      } catch (ImproperRequestException $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
         $e->logDownloadAttempt();

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";

      }
   }
}

?>

Первым шагом мы определяем новое исключение, ImproperRequestException, это исключение будет содержать метод logDownloadAttempt(), в нашем фрагменте кода он только выводит сообщение, но вы можете добавить в него необходимые действия по своему вкусу, например, отсылку предупреждения администратору системы по электронной почте и т.п. Этот метод будет использоваться в блоке catch, перехватывающем наше исключение.

Далее мы начинаем обрабатывать переменную HTTP_REFERER. Эта переменная, как правило, пересылается вместе с запросом и содержит адрес той страницы, с которой был сделан запрос. Например, если вы сделаете запрос из своего блога к ресурсам developerWorks, то в журналах сервера IBM появится запись адреса URL вашего блога именно в HTTP_REFERER.

Нас интересует имя сервера, с которого пришел запрос, поэтому мы отрезаем название протокола в начале адреса "http://" и сохраняем текст до первого символа "/". Полученная строка и будет содержать имя сервера.

Если запрос был внешний, то он имя сервера будет иметь вид, подобный boxersrevenge.nicholaschase.com. Но мы пропускаем запросы только для сервера с именем boxersrevenge или localhost, поэтому в случае внешнего запроса будет сгенерировано исключение ImproperRequestException, затем это исключение будет перехвачено и обработано в соответствующем блоке.

Следует заметить, что предложенный метод нельзя считать окончательным решением, поскольку некоторые браузеры не поддерживают пересылку обратного адреса URL запроса. Пересылка адреса, кроме того, может быть подавлена пользователем. Мы рассмотрели эту процедуру как пример подхода к обеспечению безопасности системы и контроля содержания общедоступного ресурса.


Подведем итоги

В этой, заключительной части учебного пособия, мы закончили разработку приложения, поддерживающего workflow, с которым мы работали в предыдущих двух частях серии "Изучам PHP". Две первых части были посвящены базовым темам, таким как синтаксис, работа с формами, доступ к базам данных, загрузка файлов и XML. В этой части мы собрали наши разработки в одну форму, которая позволяет администратору выполнить процедуру одобрения документов, а простому пользователю загрузить файл. Вот перечень тем, которые рассматривались в этой части:

  • Использование HTTP-аутентификации
  • Перемещение файла с использованием потока данных
  • Создание классов и объектов
  • Методы и свойства объекта
  • Конструкторы объектов
  • Использование исключений
  • Создание пользовательских исключений
  • Использование ID атрибутов XML
  • Обеспечение контроля доступа с использованием информации об источнике запроса

Ресурсы

Научиться

Получить продукты и технологии

  • Разработайте ваш следующий проект с открытым исходным кодом с использованием пробного программного обеспечения IMB, которое можно загрузить со страницы IBM trial software, или получить на DVD.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=188565
ArticleTitle=Изучаем PHP: Часть3. Аутентификация, работа с потоками данных, объекты и исключения
publish-date=01162007