Рецепты PHP

SNIPPETS 21.11.21 24.11.21 471
Бесплатные курсына главную сниппетов
Σ = 

Вывод слов в разной форме (1 день, 2 дня, 5 дней)

function numForm($number, $forma1, $forma2, $forma3){ 
       if(($number == "0") 
 or (($number >= "5") 
 and ($number <= "20")) 
 or preg_match("|[056789]$|",$number) 
    ){ 
        return "$number $forma3"; 
    } 
    
         if(preg_match("|[1]$|",$number)){ 
        return "$number $forma1"; 
    } 
 
         if(preg_match("|[234]$|",$number)){ 
        return "$number $forma2"; 
    } 
} 
 
// пример использования 
$array = array(1, 2, 5); 
foreach($array as $num){ 
 echo numForm($num, "день", "дня", "дней") . "<br/>"; 
} 
 
Сравнение дат
// текущая дата на сервере
 
$date_1 = date("Y-m-d"); 
// вторая дата, с которой будет сравнение 
$date_2 = "2014-10-21"; 
 
// перевод дат в формат timestamp 
$date_timestamp_1 = strtotime($date_1); 
$date_timestamp_2 = strtotime($date_2); 
 
// сравниваем 
if($date_timestamp_1 > $date_timestamp_2){ 
 echo "Первая дата больше"; 
}else if($date_timestamp_1 < $date_timestamp_2){ 
 echo "Вторая дата больше"; 
}else{ 
 echo "Даты равны"; 
} 
Разница между датами в днях
// первая дата(текущая) 
$date_1 = date("Y-m-d"); 
// вторая дата 
$date_2 = "2014-10-31"; 
 
// перевод дат в формат timestamp 
$date_timestamp_1 = strtotime($date_1); 
$date_timestamp_2 = strtotime($date_2); 
 
// разница в секундах 
$diff = $date_timestamp_1 - $date_timestamp_2; 
 // берем модуль, возможно значение с минусом 
$diff = abs($diff);
 
// Высчитываем количество дней 
 // 3600 сек = 1 час 
// и округляем до целых 
$diff_day = intval($diff / (3600 * 24)); 
// вывод количества дней 
echo $diff_day; 
Перевод чисел в разные системы исчисления
$dec = 123; 
echo "dec: $dec<br/>"; 
 
// перевод из десятичной системы счисления в двоичную 
$bin = decbin($dec); 
echo "bin: $bin<br/>"; 
 
// перевод из двоичной системы счисления в десятичную 
$dec = bindec($bin); 
echo "dec: $dec<br/>"; 
 
// перевод из десятичной системы счисления 
// в восьмеричную 
$oct = decoct($dec); 
echo "oct: $oct<br/>"; 
 
// перевод из восьмеричной системы счисления в 
десятичную 
$dec = octdec($oct); 
echo "dec: $dec<br/>"; 
 
// перевод из десятичной системы счисления в 
шестнадцатеричною 
$hex = dechex($dec); 
echo "hex: $hex<br/>"; 
 
// перевод из шестнадцатеричной системы счисления в 
десятичную 22 
 
$dec = hexdec($hex); 
echo "dec: $dec<br/>";
Время выполнения скрипта
// засекаем начало выполнения скрипта 
$start_time = microtime(true); 
  
// код, время которого нужно замерить 
// пример скрипта 
for($i = 0; $i<1; $i+=0.000001); 
  
 // засекаем завершение выполнения скрипта 
$finish_time = microtime(true); 
  
// высчитываем разницу во времени 
$result_time = $finish_time - $start_time; 
  
// форматированный вывод результата 
printf('Затрачено %.4F сек.', $result_time);
Отправка письма
// получатель письма 
$strTo = 'test1@test.com'; 
// Тема письма 
$subject = "Тестовое письмо"; 
 // Текст письма. 
 // Тут может быть как просто текст, так и html код 
$message = ' 
 <html> 
  <head> 
   <title>Тестовое письмо</title> 
  </head> 
  <body> 
   <p>Текст письма</p> 
  </body> 
 </html> 
'; 
// заголовок письма 
$headers= "MIME-Version: 1.0\r\n"; 
// кодировка письма 
$headers .= " 
Content-type: text/html; charset=utf-8\r\n 
"; 
// от кого письмо
 
$headers .= "From: Тестовое письмо <no-reply@test.com>\r\n"; 
// отправляем письмо 
 $result = mail($strTo, $subject, $message, $headers); 
// результат отправки письма 
if($result){ 
 echo "Письмо успешно отправлено"; 
}else{ 
 echo "Письмо не отправлено"; 
} 
Отправка письма нескольким получателям
// массив получателей письма 
$arrayTo = array( 
 'test1@test.com', 
  'test2@test.com', 
  'test3@test.com' 
); 
// переводим массив в строку 
 // и разделяем адреса запятыми 
$strTo = implode(",", $arrayTo); 
  
// Тема письма 
$subject = "Тестовое письмо"; 
 // Текст письма. 
 // Тут может быть как просто текст, так и html код 
$message = ' 
 <html> 
  <head> 
   <title>Тестовое письмо</title>
 
  </head> 
  <body> 
   <p>Текст письма</p> 
  </body> 
 </html> 
'; 
// заголовок письма 
$headers= "MIME-Version: 1.0\r\n"; 
// кодировка письма 
$headers .= " 
Content-type: text/html; charset=utf-8\r\n 
"; 
 // от кого письмо 
$headers .= "From: Тестовое письмо <no-reply@test.com>\r\n"; 
 // отправляем письмо 
 $result = mail($strTo, $subject, $message, $headers); 
// результат отправки письма 
if($result){ 
 echo "Письмо успешно отправлено"; 
}else{ 
 echo "Письмо не отправлено"; 
}
Проверка корректности e-mail адреса
// e-mail адрес, который будем проверять 
$email = "admin@test_site.com"; 
  
// Проверка e-mail адреса 
 if(preg_match(" 
|^[-0-9a-z_\.]+@[-0-9a-z_^\.]+\.[a-z]{2,6}$|i" 
, $email 
)){ 
  echo "e-mail корректный"; 
}else{ 
   echo "e-mail не корректный"; 
    }
Как определить, читали письмо или нет

Чтобы определить, читали отправленное письмо или нет, можно воспользоваться небольшой хитростью – отправить в письме картинку, которая будут подгружаться с удаленного сервера, и при обращении к этой картинке, можно реализовать вызов счетчика количества просмотров. Для того чтобы при обращении к картинке на сервере выполнялся скрипт можно воспользоваться apache модулем mod_rewrite. Он позволит реализовать редирект с картинки на скрипт. Для этого достаточно добавить в корень сайта файл .htaccess с кодом:

<IfModule mod_rewrite.c> 
     RewriteEngine on 
     RewriteRule ([[:alnum:]]+).png$ img.php?em=$1 [L] 
</IfModule> 

Таким образом, при обращении к картинке с адресом: <img src="http://site.ru/dGVzdEBtYWlsLnJ1.png"/> будет вызван скрипт http://site.ru/img.php?em=dGVzdEBtYWlsLnJ1 Как вы уже наверняка обратили внимание, помимо вызова самого скрипта, происходит передача GET параметра em – имя картинки. Это сделано специально, с помощью этого параметра можно передавать любую необходимую информацию, например e-mail адрес получателя письма. В данном примере это закодированный, с помощью base64, e-mail получателя. Остается только написать скрипт, который будет обрабатывать обращения и возвращать в ответ картинку и записывать статистику обращений:

 // генерируем картинку и отдаем ее 
 // создаем холст 1 на 1 пиксель 
$image = imagecreatetruecolor(1,1); 
 // делаем его белым 
imagefill($image, 0, 0, 0xFFFFFF); 
 // задаем заголовок для вывода картинки 
header('Content-type: image/png'); 
 // выводим картинку 
imagepng($image); 
 // очищаем память от картинки 
imagedestroy($image); 
  
 // проверяем наличие GET параметра 
if(isset($_GET['em'])){ 52 
 
 // получили email пользователя, 
 // который открыл письмо 
 // раскодирем данные 
    $email = base64_decode($_GET['em']); 
  
     // тут реализуем запись статистики 
 // в файл или базу данных 
 }
Отправка писем с вложенными файлами

Для отправки писем с вложениями достаточно использовать функцию php mail. Файлы, которые будут отправлены, необходимо закодировать в формат base64 и добавить в тело письма, а также указать в отправляемых заголовках письма информацию о том, что в письме присутствуют файлы. Чтобы отделить закодированный файл от текста письма, необходимо добавить текстовый разделитель, это может быть любая уникальная строка. Разделитель следует обозначить в отправляемых заголовках, и выводить до и после прикрепления файла в тексте письма.

// файл 
$file = "/files/file.txt"; 
 // кому письмо 53 
 
$mailTo = "zhenikipatov@yandex.ru"; 
 // от кого письмо 
$from = "test@files.com"; 
 // тема письма 
$subject = "Test file"; 
 // текст письма 
$message = "Тестовое письмо с вложением"; 
  
// разделитель в письме 
$separator = "---"; 
// Заголовки для письма 
$headers = "MIME-Version: 1.0\r\n"; 
// задаем от кого письмо 
$headers .= "From: $from\nReply-To: $from\n"; 
 // в заголовке указываем разделитель 
$headers .= "Content-Type: multipart/mixed;" . 
   "boundary=\"$separator\""; 
  
// начало тела письма, выводим разделитель 
$bodyMail = "--$separator\n"; 
 // кодировка письма 
$bodyMail .= "Content-type: text/html;" . 
     "charset='utf-8'\n"; 
 // задаем конвертацию письма 
$bodyMail .= "Content-Transfer-Encoding: quoted-printable"; 
 // задаем название файла 
$bodyMail .= "Content-Disposition: attachment;" 
     . "filename==?utf-8?B?" . 
  base64_encode(basename($file))."?=\n\n"; 
 $bodyMail .= $message."\n"; // добавляем текст письма 
$bodyMail .= "--$separator\n"; 
 
// тип контента и имя файла 
$bodyMail .= "Content-Type: application/octet-stream;" 
    . "name==?utf-8?B?" . 
  base64_encode(basename($file))."?=\n"; 
 // кодировка файла 
$bodyMail .= "Content-Transfer-Encoding: base64\n"; 
 $bodyMail .= "Content-Disposition: attachment;" . 
    "filename==?utf-8?B?" . 
 base64_encode(basename($file))."?=\n\n"; 
 
// считываем файл 
$contentFile = file_get_contents($file); 
// кодируем и прикрепляем файл 
$bodyMail .= 
chunk_split(base64_encode($contentFile))."\n"; 
 $bodyMail .= "--".$separator ."--\n"; 
 
// отправка письма 
$result = mail($mailTo, $subject, $bodyMail, 
$headers); 
 if($result){ 
 echo "Письмо успешно отправлено"; 
}else{ 
 echo "Письмо не отправлено"; 
}
Отправка писем с картинками в тексте

Не редко необходимо отправлять письма, в которых помимо текста должна быть отправлена html-верстка с картинками. Реализовать отображение картинок можно двумя способами: прописывать для картинок полные пути – загрузка изображений будет происходить с уделенного сайта. Или отправлять картинки вместе с письмом. Второй способ работает более корректно, поскольку при загрузке изображений с удаленных сайтов некоторые почтовые программы блокируют отображение. Для верстки письма используются все те же теги и стили, что и при обычной верстке, за исключением того, что стили должны находиться в самой верстке, а не в подключаемых файлах. Еще одно отличие – это то, что для изображений(img) в атрибутах src необходимо прописывать не путь, а CID изображения. CID — Content-ID будет указывать на картинку, которую необходимо предварительно закодировать в base64 и отправим вместе с письмом. Как отправлять письма с вложениями, было подробнее описано в предыдущем рецепте – «Отправка письма с вложениями». Пример:

// картинки 
$attach = array( 
    '/imgs/1.jpg', 
    '/imgs/2.jpg' 
); 
// чтобы отображалась картинка и ее не было в аттаче 
// путь к картинке задается через CID: - Content-ID 
// тестовая верстка письма 
$text = ' 
    <div style="width: 700px; margin: 0 auto;"> 
        <h1>тело письма с картинкой</h1> 
        <h2>Блок по центру</h2> 
        <p> 
        <img style="float: left;" src="cid:1.jpg" /> 
  Какой-то текст вокруг картинки. 
   Какой-то текст вокруг картинки. 
   Какой-то текст вокруг картинки. 
   Какой-то текст вокруг картинки. 
        <br/> 
        <img style="float: left;" src="cid:2.png" /> 
        Какой-то текст вокруг картинки. 
   Какой-то текст вокруг картинки. 
   Какой-то текст вокруг картинки. 
   Какой-то текст вокруг картинки. 
        </p> 
    </div> 
 
'; 
 
 // E-mail отправителя 
$from = "test@test.com"; 
// E-mail получателя 
$to = "test_2@test.com"; 
// Тема письма 
$subject = "Тема письма"; 
 
 // Заголовки письма === >>> 
$headers = "From: $from\r\n"; 
//$headers .= "To: $to\r\n"; 
$headers .= "Subject: $subject\r\n"; 
$headers .= "Date: " . date("r") . "\r\n"; 
$headers .= "X-Mailer: zm php script\r\n"; 
$headers .= "MIME-Version: 1.0\r\n"; 
$headers .="Content-Type: multipart/alternative;\r\n"; 
// генерируем базовый разделитель 
$baseboundary = "------------" . md5(microtime()); 
$headers .= "  boundary=\"$baseboundary\"\r\n"; 
// <<< ==================== 
 
 // Тело письма === >>> 
$message  =  "--$baseboundary\r\n"; 
$message .= "Content-Type: text/plain;\r\n"; 
$message .= "Content-Transfer-Encoding: 7bit\r\n\r\n"; 
$message .= "--$baseboundary\r\n"; 
// генерируем разделитель для картинок 
$newboundary = "------------" . md5(microtime()); 
$message .= "Content-Type: multipart/related;\r\n"; 
$message .= "  boundary=\"$newboundary\"\r\n\r\n\r\n"; 
$message .= "--$newboundary\r\n"; 
$message .= "Content-Type: text/html; ". 
"charset=utf-8\r\n"; 
$message .= "Content-Transfer-Encoding: 7bit\r\n\r\n"; 
$message .= $text . "\r\n\r\n"; 
// <<< ============== 
 
 // прикрепляем файлы ===>>> 
foreach($attach as $filename){ 
    $mimeType='image/png'; 
   // получаем картинку 
    $fileContent = file_get_contents($filename,true);
 
    $filename = basename($filename); 
    $message.="--$newboundary\r\n"; 
    $message.="Content-Type: $mimeType;\r\n"; 
    $message.=" name=\"$filename\"\r\n"; 
    $message.="Content-Transfer-Encoding: base64\r\n"; 
    $message.="Content-ID: <$filename>\r\n"; 
    $message.="Content-Disposition: inline;\r\n"; 
    $message.=" filename=\"$filename\"\r\n\r\n"; 
   // кодируем картинку 
  $message.= 
chunk_split(base64_encode($fileContent)); 
} 
// <<< ==================== 
 
 // заканчиваем тело письма, дописываем разделители 
$message.="--$newboundary--\r\n\r\n"; 
$message.="--$baseboundary--\r\n"; 
 
 // отправка письма 
$result = mail($to, $subject, $message , $headers); 
if($result){ 
 echo "Письмо успешно отправлено!"; 
}else{ 
 echo "Письмо не отправлено!"; 
}
Отправка писем через SMTP протокол

SMTP — сетевой протокол, предназначенный для передачи электронной почты в сетях TCP/IP. Для работы с почтовыми серверами через SMTP протокол, необходимо реализовывать обращение с помощью сокетов. Для открытия сокета используется php функция fsockopen. После подключения к почтовому серверу, необходимо «представиться» серверу – передать ему логин и пароль пользователя, которому доступна отправка почты. После этого осуществляется передача e-mail адресов отправителя и получателя. И в последнюю очередь передаются «тело» письма: заголовки и содержимое. В качестве примера, приведен код, который обращается к почтовому серверу yandex.ru:

// имя пользователя 
$smtp_username = 'username@yandex.ru'; 
// пароль 
$smtp_password = 'password'; 
// адрес smtp сервера 
$smtp_host = 'ssl://smtp.yandex.ru'; 
// порт для обращения к smtp серверу 
$smtp_port = 465; 
 
// тема письма 
$subject = "Тема письма"; 
// текст письма 
$message = "Текст письма"; 
// e-mail получателя письма 
$mailTo = "zhenikipatov@yandex.ru"; 
 
// заголовок письма 
$headers = "MIME-Version: 1.0\r\n"; 
// кодировка письма 
$headers .= "Content-type: text/html; " . 
    "charset=utf-8\r\n"; 
 // от кого письмо 
$headers .= "From: Evgeniy <admin@vk-book.ru>\r\n"; 
  
// тело письма 
$contentMail = "Date: " . date("D, d M Y H:i:s") 
 . " UT\r\n"; 
$contentMail .= 'Subject: =?UTF-8?B?' . 
     base64_encode($subject) . 
"=?=\r\n"; 
$contentMail .= $headers . "\r\n"; 
$contentMail .= $message . "\r\n"; 
 
// соединение с почтовым сервером через сокет 
if(!$socket = @fsockopen(
 
    $smtp_host, 
     $smtp_port, 
     $errorNumber, 
     $errorDescription, 
     30) 
){ 
 // если произошла ошибка 
 die($errorNumber.".".$errorDescription); 
} 
 
// проверяем ответ сервера, если код 220 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "220")){ 
  die('Ошибка соединения'); 
} 
 
// представляемся почтовому серверу, 
// передаем ему адрес своего хоста 
$server_name = $_SERVER["SERVER_NAME"]; 
fputs($socket, "HELO $server_name\r\n"); 
// проверяем ответ сервера, если код 250 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "250")){ 
 fclose($socket); 
 die('Ошибка при приветствии'); 
} 
 
// начинаем авторизацию на почтовом сервере 
fputs($socket, "AUTH LOGIN\r\n"); 
// проверяем ответ сервера, если код 334 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "334")){ 
 fclose($socket); 
 die('Ошибка авторизации'); 
} 
 
 // отправляем почтовому серверу логин, 
// через который будем авторизовываться 
fputs($socket, base64_encode($smtp_username)."\r\n"); 
// проверяем ответ сервера, если код 334 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "334")){ 
 fclose($socket);
 
 die('Ошибка авторизации'); 
} 
 
// отправляем почтовому серверу пароль 
fputs($socket, base64_encode($smtp_password)."\r\n"); 
// проверяем ответ сервера, если код 235 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "235")){ 
 fclose($socket); 
 die('Ошибка авторизации'); 
} 
 
// сообщаем почтовому серверу e-mail отправителя 
fputs($socket, "MAIL FROM: <".$smtp_username.">\r\n"); 
// проверяем ответ сервера, если код 250 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "250")){ 
 fclose($socket); 
 die('Ошибка установки отправителя'); 
} 
 
// сообщаем почтовому серверу e-mail получателя 
fputs($socket, "RCPT TO: <" . $mailTo . ">\r\n"); 
     // проверяем ответ сервера, если код 250 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "250")){ 
 fclose($socket); 
 die('Ошибка установки получателя'); 
} 
 
// сообщаем почтовому серверу, 
 // что сейчас начнем передавать данные письма 
fputs($socket, "DATA\r\n"); 
   // проверяем ответ сервера, если код 354 
// значит все прошло успешно 
  if (!parseSocketAnswer($socket, "354")){ 
 fclose($socket); 
 die('Ошибка при передачи данных письма'); 
} 
 
// передаем почтовому серверу данные письма 
fputs($socket, $contentMail."\r\n.\r\n"); 
// проверяем ответ сервера, если код 250 
 
// значит все прошло успешно 
 if (!parseSocketAnswer($socket, "250")){ 
 fclose($socket); 
 die("Ошибка при передачи данных письма"); 
} 
 
// сообщаем почтовому серверу, 
 // что закрываем соединение 
fputs($socket, "QUIT\r\n"); 
// закрываем соединение 
fclose($socket); 
 
// результат отправки 
echo "Письмо успешно отправлено"; 
 
 
// функция, которая будет анализировать ответ 
 // почтового сервера 
// Ищет в ответе сервера необходимый код 
function parseSocketAnswer($socket, $response) { 
 while (@substr($responseServer, 3, 1) != ' ') { 
  if (!($responseServer = fgets($socket, 256))){ 
   return false; 
  } 
 } 
 if (!(substr($responseServer, 0, 3) == $response)) { 
  return false; 
 } 
 return true; 
}
Получить письма. Пример работы с IMAP протоколом

IMAP — протокол для доступа к электронной почте. Через этот протокол можно получать любую информацию о почте пользователя. Для работы с почтовым сервером через протокол IMAP в php существует много функций. В примере используем только несколько основных: imap_open – открывает соединение с почтовым сервером по протоколу IMAP. imap_search – осуществляет поиск писем по заданным параметрам, например, «NEW» - найдет все новые. И возвращает массив номеров писем. imap_header – возвращает заголовки письма по его номеру. imap_fetchbody – получает содержимое «тела» письма по его номеру. imap_close – закрывает соединение с почтовым сервером. imap_last_error – возвращает последнюю IMAP-ошибку

// логин 
$email = "username@yandex.ru"; 
// пароль 
$password = "password"; 
// соединяемся с почтовым сервером, 
 // в случае ошибки выведем ее на экран 
$connect_imap = imap_open(" 
 {imap.yandex.ru:993/imap/ssl}INBOX", 
 $email, 
 $password 
) or die("Error:" . imap_last_error()); 
// проверим ящик на наличие новых писем 
$mails = imap_search($connect_imap, 'NEW'); 
// если есть новые письма 
if($mails){ 
 // перебираем все письма 
 foreach($mails as $num_mail){ 
  // получаем заголовок 
  $header = imap_header( 
$connect_imap, $num_mail 
); 
  // достаем ящик отправителя письма 
  $mail_from = $header->sender[0]->mailbox . 
 "@" . $header->sender[0]->host;
 
  echo "От кого: $mail_from <br/>"; 
  // получаем тему письма 
  $subject = $header->subject; 
  echo "Тема письма: $subject <br/>"; 
  // получаем содержимое письма 
  $text_mail = imap_fetchbody( 
$connect_imap, $num_mail, 1 
); 
   echo "Тело письма: $text_mail <br/>"; 
  echo "<hr/>"; 
 } 
}else{ 
 echo "Нет новых писем"; 
} 
// закрываем соединение 
imap_close($connect_imap);
Размер файла. Перевод байт в КБ, Мб и тд

Содержимое спойлера

// размер файла 
// путь до файла и его название 
$file_name = "/file.txt"; 
$size = filesize($file_name); 
// вызываем функцию для форматирования размера файла 
echo format_size($size); 
  
// функция форматирует вывод размера файла 
function format_size($size){ 
 $metrics[0] = 'байт'; 
 $metrics[1] = 'Кбайт'; 
 $metrics[2] = 'Мбайт'; 
 $metrics[3] = 'Гбайт'; 
 $metrics[4] = 'Тбайт'; 
 $metric = 0; 
          while(floor($size / 1024) > 0){ 
  $metric ++; 
  $size /= 1024; 
 } 
         $result = round($size, 1) . " " . 
   (isset($metrics[$metric]) ? $metrics[$metric] : '???'); 
 return $result; 
}
Удаление временных файлов

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

// путь до папки с временными файлами 
$folderPath = "/tmp"; 
 $count = 0; // счетчик файлов 
// проверяем существование 
 if (is_dir($folderPath)) { 
  // открываем папку 
 if ($dir = opendir($folderPath)) { 
   // перебираем все файлы 
  while (($file = readdir($dir)) !== false) { 
    // если это файл 74 
 
   if($file !='.' && $file !='..'){ 
     // то удаляем его 
    if(unlink($folderPath.'/'.$file)){ 
      // вывод имени 
 // удаленного файла 
     echo " 
   File: $file removed<br/> 
"; 
       $count ++; 
     } 
   } 
  } 
   // закрываем папку 
  closedir($dir); 
  } 
 } 
  
// выводим количество удаленных файлов 
echo "Count remove: $count"; 

Как правило, скрипты, подобные тому, что приведен в примере, устанавливаются на cron – планировщик задач. К примеру, скрипт автоматически вызывается раз в сутки и удаляет все временные файлы.

Простое сжатие CSS файлов

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

// массив с путями до css файлов 
$css_array = array( 
    'css/style_1.css', 
    'css/style_2.css' 
); 
// путь, куда будет сохранен сжатый файл 
$new_file = "css/compression_file.css"; 
// вызываем функцию сжатия 
$result = compression_files($css_array, $new_file); 
var_dump($result); // вывод результата 
 
/** 
*   Функция для сжатия CSS файлов 
*   Удаляет комментарии, табуляцию, 
 *   переходы на новую строку и повторяющиеся пробелы 
*   А также собирает все файлы в один 
* 
*   @var $files_css array  - массив путей 
*   до css файлов, которые необходимо сжать 
* 
*   @var $new_file  string - путь, куда будет 
 *   сохранен сжатый файл 
* 
 
*   @return bool - результат 
*/ 
function compression_files($files_css, $new_file) { 
    // получаем содержимое всех css файлов 
    $content_css = ""; 
    foreach($files_css as $one_file){ 
  $content_css .= @file_get_contents($one_file); 
  // если какой-то из файлов 
 // не получилось прочитать 
  if(!$content_css) return false; 
     } 
     // удаляем комментарии 
     $content_css = preg_replace( 
'!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', 
$content_css 
   ); 
    // удаляем табуляции и переходы на новую строку 
    $content_css = str_replace( 
  array("\r\n", "\r", "\n", "\t"), ' ', 
$content_css 
   ); 
    // удаляем повторяющиеся пробелы 
    $content_css = preg_replace( 
'/ {2,}/', ' ', $content_css 
  ); 
         // сохраняем результат в файл 
    $css_file = fopen ($new_file, "w+"); 
      fwrite($css_file, $content_css); 
      $result_save = fclose($css_file); 
          // вернем результат сохранения 
    return $result_save; 
}
Массовая замена текста в файлах
// пример использования 
$oldText = 'old text'; // что меняем 
 
$newText = 'new text'; // на что меняем 
$folderName = "./files"; // в какой папке меняем 
replace_txt($folderName, $oldText, $newText); 
 
/** 
* Функция замены текста во всех файлах папки 
* 
 * @param string $folderName - пусть до папки 
* @param string $oldText - искомый текст 
* @param string $newText - на что меняем текст 
*/ 
function replace_txt($folderName, $oldText, $newText){ 
  // открываем текущую папку 
   $dir = opendir($folderName); 
   // перебираем папку 
   // перебираем пока есть файлы 
  while (($file = readdir($dir)) !== false){ 
    // если это не папка 
   if($file != "." && $file != ".."){ 
      // если файл 
     if(is_file($folderName."/".$file)){ 
   // открываем файл 
  $contentFile = file_get_contents( 
    $folderName."/".$file 
); 
   // для работы с файлами в 
 // кодировке windows-1251 
  //$contentFile = iconv( 
// "windows-1251", "utf-8", $contentFile 
//); 
   // делаем замену в тексте 
  $contentFile = str_replace( 
$oldText, $newText, $contentFile 
); 
 // сохраняем изменения 
     file_put_contents( 
    $folderName."/".$file,$contentFile 
); 
      } 
      // если папка, то рекурсивно 
     // вызываем replace_txt 
     if(is_dir($folderName."/".$file)){
 
       replace_txt( 
  $folderName."/".$file, $oldText, $newText 
            ); 
     } 
   } 
   } 
   // закрываем папку 
  closedir($dir); 
 }
Генерация случайной капчи

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

// список символов, используемых в капче 
$let = '0123456789ABCDEFGH'; 
  // количество символов в капче 
$len = 4; 
// шрифт 
$font = 'impact.ttf'; 
 // Размер шрифта 
 $fontsize = 20; 
 // Размер капчи 
$width = 100; 
$height = 30; 
 
// создаем изображение 
$img = imagecreatetruecolor($width, $height); 
  // фон 
$white = imagecolorallocate($img, 220, 220, 220); 
 imagefill($img, 0, 0, $white); 
 // Переменная, для хранения значения капчи 
$capchaText = ''; 
  
 // Заполняем изображение символами 
for ($i = 0; $i < $len; $i++){ 
     // Из списка символов, берем случайный символ 
     $capchaText .= $let[rand(0, strlen($let)-1)]; 
      // Вычисляем положение одного символа 
    $x = ($width - 20) / $len * $i + 10; 
     $y = $height - (($height - $fontsize) / 2); 
         // Укажем случайный цвет для символа 
    $color = imagecolorallocate( 
$img, rand(0, 150), 
 rand(0, 150), rand(0, 150) 
   ); 
      // Генерируем угол наклона символа 
     $naklon = rand(-30, 30); 
      // Рисуем символ 
    imagettftext( 
  $img, $fontsize, $naklon, $x, 
   $y, $color, $font, $capchaText[$i] 
  ); 
 } 
 
// заголовок для браузера 
header('Content-type: image/png');
 
// вывод капчи на страницу 
imagepng($img); 
  // чистим память 
imagedestroy($img);
Генерация арифметической капчи

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

// шрифт 
$font = 'impact.ttf'; 
 // Размер шрифта 
 $fontsize = 20; 
 // Размер капчи 
$width = 120; 
$height = 40; 
 
// придумываем пример для капчи 
$a = mt_rand(1, 19); 
$b = mt_rand(1, 19); 
$capchaText = $a . '+' . $b . '='; 
// Ответ на пример 
$capchaResult = $a + $b; 
 
// создаем изображение 
$img = imagecreatetruecolor($width, $height); 
  // фон 
$white = imagecolorallocate($img, 220, 220, 220); 
 imagefill($img, 0, 0, $white); 
  
 // Заполняем изображение символами 
for ($i = 0; $i < strlen($capchaText); $i++){ 
     // Из списка символов, берем случайный символ 
     $litteral = $capchaText[$i]; 
 
    // Вычисляем положение одного символа 
    $x = ($width - 20) / strlen($capchaText) * $i +10; 
     $y = $height - (($height - $fontsize) / 2); 
         // Сгенерируем случайный цвет для символа. 
     $color = imagecolorallocate( 
$img, rand(0, 150), 
 rand(0, 150), rand(0, 150) 
   ); 
      // Генерируем угол наклона символа 
     $naklon = rand(-10, 10); 
     // Рисуем один символ 
    imagettftext( 
$img, $fontsize, $naklon, $x, $y, 
 $color, $font, $litteral 
  ); 
 } 
 
// Добавим на капчу несколько рандомных полосок 
for ($i = 0; $i < $countLine; $i++){ 
     // сгенерируем координаты для линии 
    $part = $width/100; // длина картинки в процентах 
  // x1 не больше чем до 30% картинки 
    $x1 = mt_rand(0, round($part*30)); 
     $y1 = mt_rand(0, $height); 
   // x2 не меньше чем от 70% картики 
    $x2 = mt_rand(round($part*70), round($part*100)); 
     $y2 = mt_rand(0, $height); 
    // сгенерируем случайный цвет для линии 
    $color = imagecolorallocate( 
$img, rand(0, 150), 
 rand(0, 150), rand(0, 150) 
   ); 
      imageline ($img, $x1, $y1, $x2, $y2, $color); 
} 
 
// заголовок для браузера 
header('Content-type: image/png'); 
 // вывод капчи на страницу 
imagepng($img); 
  // чистим память 
imagedestroy($img);
128 рецептов php128 рецептов php 152 страницы · 2016 · 1.21 MB · русский by Ипатов Е.С.
 
Эффективный перебор больших или высокозатратных наборов данных

Задача:
Требуется перебрать содержимое списка данных. При этом весь список занимает много памяти или очень медленно строится.
Решение:
Воспользуйтесь генератором:

function FileLineGenerator($file) {
    if (!$fh = fopen($file, 'r')) {
        return;
    }
    while (false !== ($line = fgets($fh))) {
        yield $line;
    }
    fclose($fh);
}
$file = FileLineGenerator('log.txt');
foreach ($file as $line) {
    if (preg_match('/^rasmus: /', $line)) { print $line; }
}

Комментарий
Генераторы предоставляют простой механизм эффективного перебора без затрат, связанных с загрузкой всех данных в память. Поддержка генераторов появилась в PHP 5.5.
Генератор представляет собой функцию, которая возвращает итеративный объект. При переборе по объекту PHP многократно вызывает генератор для получения следующего значения, которое возвращается функцией-генератором при помощи ключевого слова yield.
В отличие от нормальных функций, которые каждый раз начинают выполнение с нуля, PHP сохраняет текущее состояние функции между вызовами генератора. Это позволяет сохранить любую информацию, необходимую для получения следующего генерируемого значения.
Если данных больше нет, функция просто возвращает управление без команды return, даже пустой (попытка вызова return в генераторе недопустима).
Идеальный пример использования генератора — обработка всех строк файла. Проще всего воспользоваться функцией file(): она открывает файл, загружает каждую строку в элемент массива и закрывает файл. Однако при этом весь файл хранится в памяти.

$file = file('log.txt');
foreach ($file as $line) {
    if (preg_match('/^rasmus: /', $line)) { print $line; }
}

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

function print_matching_lines($file, $regex) {
    if (!$fh = fopen('log.txt','r')) {
        return;
    }
    while(false !== ($line = fgets($fh))) {
        if (preg_match($regex, $line)) { print $line; }
    }
    fclose($fh);
}
print_matching_lines('log.txt', '/^rasmus: /');

Но если упаковать код обработки файла в генератор, вы получаете и то и другое — обобщенную функцию для эффективного перебора строк файла и четкость син- таксиса, как если бы все данные хранились в массиве:

function FileLineGenerator($file) {
    if (!$fh = fopen($file, 'r')) {
        return;
    }
    while (false !== ($line = fgets($fh))) {
        yield $line;
    }
    fclose($fh);
}
$file = FileLineGenerator('log.txt');
foreach ($file as $line) {
    if (preg_match('/^rasmus: /', $line)) { print $line; }
}

В генераторе управление передается между циклом и функцией командой yield.
При первом вызове генератора выполнение начинается от начала функции и приостанавливается при достижении команды yield, возвращающей значение.
В приведенном примере функция-генератор FileLineGenerator() перебирает строки файла. После открытия файла функция fgets() вызывается в цикле. Пока еще остаются непрочитанные строки, цикл возвращает итератору $line. В конце файла цикл останавливается, файл закрывается, а функция завершается. Так как yield не выполняется, происходит выход из foreach.
Теперь функция FileLineGenerator() может использоваться везде, где потребуется выполнить перебор строк файла. Приведенный пример выводит строки, начинающиеся с префикса rasmus:.
Следующий пример выводит случайную строку из файла:

$line_number = 0;
foreach (FileLineGenerator('sayings.txt') as $line) {
    $line_number++;
    if (mt_rand(0, $line_number - 1) == 0) {
        $selected = $line;
    }
}
print $selected . "\n";

Как видите, в совершенно другом сценарии использования FileLineGenerator() используется повторно без каких-либо изменений. На этот раз генератор не сохраняется в переменной, а вызывается из цикла foreach.
«Вернуть» генератор обратно невозможно. Генераторы работают только в одном направлении.

Построение строки запроса

Задача
Требуется сгенерировать строку запроса с заданными парами «имя/значение».
Решение
Воспользуйтесь функцией http_build_query():

$vars = array('name' => 'Oscar the Grouch',
              'color' => 'green',
              'favorite_punctuation' => '#');8 .4 . Построение строки запроса 261
$query_string = http_build_query($vars);
$url = '/muppet/select.php?' . $query_string;

Комментарий
URL-адрес, построенный в Решении, выглядит так: /muppet/select.php?name=Oscar+the+Grouch&color=green&favorite_punctuation=%23

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

Задача
Требуется использовать PHP для защиты частей сайта паролями. Вместо того чтобы хранить пароли во внешнем файле и поручить аутентификацию веб-серверу, вы хотите реализовать логику проверки пароля в программе PHP.
Решение
Суперглобальные переменные $_SERVER['PHP_AUTH_USER'] и $_SERVER['PHP_AUTH_PW'] содержат имя пользователя и пароль (если они были предоставлены пользователем). Чтобы запретить доступ к странице, отправьте заголовок WWW-Authenticate, идентифицирующий защищенную область как часть ответа с кодом статуса HTTP 401:

http_response_code(401);
header('WWW-Authenticate: Basic realm="My Website"');
echo "You need to enter a valid username and password.";
exit();

Комментарий
Получив заголовок 401, браузер отображает диалоговое окно для ввода имени пользователя и пароля. Эти учетные данные (имя пользователя и пароль), если они принимаются сервером, связываются с защищенной областью в заголовке WWW-Authenticate. Код, проверяющий учетные данные аутентификации, должен выполняться до отправки какого-либо вывода браузеру, потому что из него могут отправляться заголовки. Например, можно воспользоваться такой функцией, как validate() из листинга 8.1. Листинг 8 .1 . validate() 

function validate($user, $pass) {
    /* Заменить соответствующей проверкой имени пользователя
       и пароля - например, проверкой по базе данных */
    $users = array('david' => 'fadj&32',
                   'adam'  => '8HEj838');
    if (isset($users[$user]) && ($users[$user] === $pass)) {
        return true;
    } else {
        return false;
    }
}

В листинге 8.2 приведен пример использования validate(). Листинг 8 .2 . Использование функции проверки 

if (! validate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
    http_response_code(401);
    header('WWW-Authenticate: Basic realm="My Website"');
    echo "You need to enter a valid username and password.";
    exit;
}

Содержимое функции validate() следует заменить логикой проверки правильности пароля. Также можно изменить строку защищенной области и текст сообщения, которое должно выводиться в случае нажатия кнопки отмены в окне аутентификации браузера.
Кроме базовой аутентификации, PHP также поддерживает дайджест-аутентификацию. При базовой аутентификации имена и пароли отправляются в незащищенном виде по сети, с минимальной маскировкой в виде кодирования Base64.
При дайджест-аутентификации сам пароль никогда не передается от браузера к серверу — отправляется только хеш-код пароля вместе с другими значениями. Тем самым сокращается вероятность перехвата сетевого трафика и его воспроизведения атакующей стороной. Повышенная безопасность, обеспечиваемая дайджест-аутентификацией, означает, что код реализации будет сложнее простого сравнения паролей. В листинге 8.3 представлены функции для реализации дайджест-аутентификации в соответствии с RFC 2617. Листинг 8 .3 . Использование дайджест-аутентификации

/* Заменить соответствующей проверкой имени пользователя
   и пароля - например, проверкой по базе данных */
$users = array('david' => 'fadj&32',
               'adam'  => '8HEj838');
$realm = 'My website';
$username = validate_digest($realm, $users);
// При недействительных данных аутентификации управление
// никогда не достигнет этой точки.
print "Hello, " . htmlentities($username);
function validate_digest($realm, $users) {
    // Ошибка, если клиент не предоставил дайджест
    if (! isset($_SERVER['PHP_AUTH_DIGEST'])) {
        send_digest($realm);
    }
    // Ошибка, если дайджест не удается разобрать
    $username = parse_digest($_SERVER['PHP_AUTH_DIGEST'], $realm, $users);
    if ($username === false) {
        send_digest($realm);
    }
    // В дайджесте указано правильное имя пользователя
    return $username;
}
function send_digest($realm) {
    http_response_code(401);
    $nonce = md5(uniqid());
    $opaque = md5($realm);
    header("WWW-Authenticate: Digest realm=\"$realm\" qop=\"auth\" ".
           "nonce=\"$nonce\" opaque=\"$opaque\"");
    echo "You need to enter a valid username and password.";
    exit;
}
function parse_digest($digest, $realm, $users) {
    // Необходимо найти в заголовке дайджеста следующие значения:
    // username, uri, qop, cnonce, nc и response
    $digest_info = array();
    foreach (array('username','uri','nonce','cnonce','response') as $part) {
        // Ограничителем может быть символ ' или " или ничего (для qop и nc)
        if (preg_match('/'.$part.'=([\'"]?)(.*?)\1/', $digest, $match)) {
            // Если компонент найден, сохранить его для вычислений
            $digest_info[$part] = $match[2];
        } else {
            // Если компонент отсутствует, проверка дайджеста невозможна;
            return false;
        }
    }
    // Убедиться в правильности предоставленного значения qop
    if (preg_match('/qop=auth(,|$)/', $digest)) {
        $digest_info['qop'] = 'auth';
    } else {
        return false;
    }
    // Убедиться в правильности переданного временного значения
    if (preg_match('/nc=([0-9a-f]{8})(,|$)/', $digest, $match)) {
        $digest_info['nc'] = $match[1];
    } else {
        return false;
    }
    // Теперь убедиться в том, что из заголовка дайджеста были извлечены
    // все необходимые значения, и выполнить алгоритмические вычисления,
    // необходимые для проверки правильности информации.
    //
    // Эти вычисления описаны в разделах 3.2.2, 3.2.2.1 
    // и 3.2.2.2 документа RFC 2617.
    // Алгоритм MD5
    $A1 = $digest_info['username'] . ':' . $realm . ':' .
        $users[$digest_info['username']];
    // qop содержит 'auth'
    $A2 = $_SERVER['REQUEST_METHOD'] . ':' . $digest_info['uri'];
    $request_digest = md5(implode(':', array(md5($A1), $digest_info['nonce'],
        $digest_info['nc'],
    $digest_info['cnonce'], $digest_info['qop'], md5($A2))));
    // Отправленные данные соответствуют вычисленным?
    if ($request_digest != $digest_info['response']) {
        return false;
    }
    // Все нормально, вернуть имя пользователя
    return $digest_info['username'];
}

Ни базовая аутентификация, ни дайджест-аутентификация HTTP не могут использоваться при выполнении PHP как программы CGI. Если вы не можете выполнять PHP в виде серверного модуля, используйте cookie-аутентификацию.
Другой недостаток аутентификации HTTP заключается в том, что она не предоставляет простых средств для завершения сеанса, кроме выхода из браузера.
В электронной документации PHP приводятся некоторые рекомендации для организации завершения сеанса, работающие с разной степенью успеха для разных комбинацией веб-серверов и браузеров.
Однако существует прямолинейный способ принудительного завершения сеанса пользователя с истечением заданного интервала времени: включение времени в строку защищенной области. Браузеры используют одну комбинацию имени пользователя и пароля каждый раз, когда у них запрашиваются учетные данные для одной защищенной области. Изменение имени защищенной области заставляет браузер запросить у пользователя новые учетные данные. В листинге 8.4 используется базовая аутентификация с принудительным завершением сеанса ежедневно в полночь. Листинг 8 .4 . Принудительное завершение сеанса с базовой аутентификацией

if (! validate($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])) {
    $realm = 'My Website for '.date('Y-m-d');
    http_response_code(401);
    header('WWW-Authenticate: Basic realm="'.$realm.'"');
    echo "You need to enter a valid username and password.";
    exit;
}

Также можно организовать тайм-аут уровня конкретного пользователя без изменения имени защищенной области; для этого следует сохранить время начала сеанса или обращения к защищенной странице. Функция validate_date() в листинге 8.5 сохраняет время начала сеанса в базе данных и принудительного завершения сеанса по истечении более 15 минут с момента последнего запроса защищенной страницы пользователем. Листинг 8 .5 . validate_date()

function validate_date($user,$pass) {
    $db = new PDO('sqlite:/databases/users');
    // Подготовка и выполнение
    $st = $db->prepare('SELECT password, last_access
                        FROM users WHERE user LIKE ?');
    $st->execute(array($user));
    if ($ob = $st->fetchObject()) {
        if ($ob->password == $pass) {
            $now = time();
            if (($now - $ob->last_access) > (15 * 60)) {
                return false;
            } else {
                // Обновление времени последнего обращения
                $st2 = $db->prepare('UPDATE users SET last_access = "now"
-                                    WHERE user LIKE ?');
                $st2->execute(array($user));
                return true;
            }
        }
    }
    return false;
}
Аутентификация с использованием cookie

Задача
Требуется усилить контроль за процедурой начала сеанса, например вывести собственную форму для ввода учетных данных.
Решение
Сохраните статус аутентификации в cookie или как часть сеансовых данных. Если процедура подключения пользователя проходит успешно, сохраните его имя пользователя (или другое уникальное значение) в cookie. Также включите хеш имени пользователя и секретного слова, чтобы пользователь не мог просто подделать cookie аутентификации с именем:

$secret_word = 'if i ate spinach';
if (validate($_POST['username'],$_POST['password'])) {
    setcookie('login',
              $_POST['username'].','.md5($_POST['username'].$secret_word));
}

Комментарий При использовании аутентификации с cookie приходится выводить собственную форму ввода учетных данных вроде той, что представлена в листинге 8.6. Листинг 8 .6 . Пример формы ввода учетных данных аутентификации

<form method="POST" action="login.php">
Username: <input type="text" name="username"> <br>
Password: <input type="password" name="password"> <br>
<input type="submit" value="Log In">
</form>

Для проверки имени пользователя и пароля можно воспользоваться функцией validate() из листинга 8.1. Единственное различие заключается в том, что этой функции в качестве учетных данных передаются значения $_POST['username'] и $_POST['password'] вместо $_SERVER['PHP_AUTH_USER'] и $_SERVER['PHP_AUTH_ PW']. Если пароль проходит проверку, сервер отправляет cookie с именем пользователя, хеш-кодом имени пользователя и секретным словом. Хеш-код предотвращает фальсификацию посредством отправки cookie с именем пользователя.
После того как пользователь пройдет проверку, странице достаточно проверить действительность отправленного значения cookie для выполнения специальных операций для этого пользователя. Листинг 8.7 демонстрирует один из способов решения этой задачи.
Листинг 8 .7 . Проверка cookie

unset($username);
if (isset($_COOKIE['login'])) {
    list($c_username, $cookie_hash) = split(',', $_COOKIE['login']);
    if (md5($c_username.$secret_word) == $cookie_hash) {
        $username = $c_username;
    } else {
        print "You have sent a bad cookie.";
    }
}
if (isset($username)) {
    print "Welcome, $username.";
} else {
    print "Welcome, anonymous user.";
}

Если вы используете встроенную поддержку сеансов, добавьте имя пользователя и хеш в сеанс и избегайте отправки отдельных cookie. При подключении нового пользователя установите дополнительную переменную в сеансе вместо того, чтобы отправлять cookie, как показано в листинге 8.8. Листинг 8 .8 . Хранение информации подключения в сеансе

if (validate($_POST['username'],$_POST['password'])) {
    $_SESSION['login'] =
        $_POST['username'].','.md5($_POST['username'].$secret_word);
}

Код проверки, приведенный в листинге 8.9, почти не изменился; просто на этот раз вместо $_COOKIE в нем используется $_SESSION. Листинг 8 .9 . Проверка сеансовой информации

unset($username);
if (isset($_SESSION['login'])) {
    list($c_username,$cookie_hash) = explode(',',$_SESSION['login']);
    if (md5($c_username.$secret_word) == $cookie_hash) {
        $username = $c_username;
    } else {
        print "You have tampered with your session.";
    }
}

Применение аутентификации с использованием cookie или сеансовых данных вместо базовой аутентификации HTTP существенно упрощает завершение работы пользователя: достаточно удалить его cookie или переменную из данных сеанса. Другое преимущество хранения аутентификационной информации в сеансе заключается в том, что вы можете связать действия пользователей после подключения с их действиями до подключения или после завершения сеанса. С базовой аутентификацией HTTP невозможно связать запросы, ассоциированные с именем пользователя, с запросами, выданными тем же пользователем до ввода учетных данных. Поиск запросов с того же IP-адреса ненадежен, особенно если пользователь находится за брандмауэром или прокси-сервером. Если вы используете сеансовые данные, можно изменить процедуру подключения и сохранять информацию о связи идентификатора сеанса с именем пользователя с применением кода, приведенного в листинге 8.10. Листинг 8 .10 . Сохранение информации о входе и выходе

if (validate($_POST['username'], $_POST['password'])) {
    $_SESSION['login'] =
        $_POST['username'].','.md5($_POST['username'].$secret_word);
    error_log('Session id '.session_id().' log in as '.$_POST['username']);
}

Листинг 8.10 записывает сообщение в журнал, но с таким же успехом он мог бы записывать информацию в базу данных, используемую при анализе трафика и использования сайта. Одна из опасностей использования сеансовых идентификаторов заключается в том, что сеансы могут перехватываться. Если Алиса угадает идентификатор сеанса Боба, она сможет выдать себя за Боба перед веб-сервером. Сеансовый модуль имеет две необязательные конфигурационные директивы, которые затрудняют подбор идентификаторов сеанса. Директива session.entropy_file содержит путь к устройству или файлу, генерирующему элемент случайности, например /dev/random или /dev/urandom. Директива session.entropy_length со- держит количество байтов, читаемых из файла энтропии при создании идентификатора сеанса. Как бы вы ни затрудняли подбор идентификаторов сеансов, они могут быть похищены при пересылке в виде простого текста между сервером и браузером пользователя. Базовой аутентификации HTTP также присуща эта проблема. Используйте SSL для защиты от анализа сетевого ттрафик.

Чтение заголовка HTTP

Задача
Требуется прочитать заголовок из запроса HTTP.270 Глава 8 . Основы веб-программирования
Решение
Если вам нужен одиночный заголовок, обратитесь к суперглобальному массиву $_SERVER:

// Заголовок User-Agent
echo $_SERVER['HTTP_USER_AGENT'];
Для получения всех заголовков вызовите функцию getallheaders():
$headers = getallheaders();
echo $headers['User-Agent'];
Принудительная отправка вывода браузеру

Задача Требуется форсировать отправку вывода браузеру. Например, перед выполнени- ем медленного запроса к базе данных вы хотите передать пользователю инфор- мацию об изменении текущего состояния.8 .13 . Буферизация вывода 275 Решение Воспользуйтесь функцией flush():

print 'Finding identical snowflakes...';
flush();
$sth = $dbh->query(
    'SELECT shape,COUNT(*) AS c FROM snowflakes GROUP BY shape HAVING c > 1');

Комментарий Функция flush() отправляет вывод, накопленный во внутреннем буфере PHP, веб-серверу. Впрочем, веб-сервер может использовать собственную внутреннюю буферизацию, которая откладывает момент получения данных браузером. Кроме того, некоторые браузеры не отображают данные непосредственно при получении, а некоторые версии Internet Explorer не выводят страницу, пока не получат минимум 256 байт. Чтобы заставить IE вывести страницу, выведите пробелы в начале страницы, как показано в листинге 8.14. Листинг 8 .14 . Принудительный вывод контента в IE

print str_repeat(' ',300);
print 'Finding identical snowflakes...';
flush();
$sth = $dbh->query(
    'SELECT shape,COUNT(*) AS c FROM snowflakes GROUP BY shape HAVING c > 1');
Программа: (де)активизация учетных записей

Когда пользователь регистрируется на вашем сайте, будет полезно убедиться в том, что он ввел правильный адрес электронной почты. Чтобы проверить адрес, отправьте сообщение по указанному адресу. Если пользователь за несколько дней не посетит специальный URL-адрес, включенный в сообщение, его учетная запись деактивизируется. Система состоит из трех частей. Первая часть — программа notify-user .php, которая отправляет сообщение новому пользователю и предлагает ему посетить проверочный URL-адрес, — приведена в листинге 8.18. Вторая часть (листинг 8.19) — страница verify-user .php — обрабатывает проверочный URL-адрес и помечает пользователей как прошедших проверку. Третья часть — программа delete-user .php — деактивизирует учетные записи пользователей, не посетивших проверочный URL-адрес за определенный промежуток времени. Эта программа приведена в листинге 8.20. Код SQL в листинге 8.17 создает таблицу для хранения информации о пользователях.
Листинг 8 .17 . Код SQL для создания таблицы проверки пользователей

CREATE TABLE users (
 email VARCHAR(255) NOT NULL,
 created_on DATETIME NOT NULL,
 verify_string VARCHAR(16) NOT NULL,
 verified TINYINT UNSIGNED
);

В листинге 8.17 определяется минимальный объем информации, необходимой для проверки пользователей. Возможно, вы захотите хранить более подробные описания. При создании учетной записи пользователя сохраните информацию в таблице users и отправьте пользователю сообщение с предложением подтвердить регистрацию. Код в листинге 8.18 предполагает, что адрес электронной почты пользователя хранится в переменной $email. Листинг 8 .18 . notify-user .php

// Подключение к базе данных
$db = new PDO('sqlite:users.db');
$email = 'david';
// Генерирование verify_string
$verify_string = '';
for ($i = 0; $i < 16; $i++) {
    $verify_string .= chr(mt_rand(32,126));
}
// Вставка записи пользователя в базу данных.
// При этом используется функция datetime(), специфическая для SQLite
$sth = $db->prepare("INSERT INTO users " .
                    "(email, created_on, verify_string, verified) " .
                    "VALUES (?, datetime('now'), ?, 0)");
$sth->execute(array($email, $verify_string));
$verify_string = urlencode($verify_string);
$safe_email = urlencode($email);
$verify_url = "http://www.example.com/verify-user.php";
$mail_body=<<<_MAIL_
To $email:
Please click on the following link to verify your account creation:
$verify_url?email=$safe_email&verify_string=$verify_string
If you do not verify your account in the next seven days, it will be
deleted.
_MAIL_;
mail($email,"User Verification",$mail_body);

Проверочная страница, на которую направляется пользователь по ссылке из сообщения, обновляет таблицу users при получении правильной информации (листинг 8.19).
Листинг 8 .19 . verify-user .php

// Подключение к базе данных
$db = new PDO('sqlite:users.db');
$sth = $db->prepare('UPDATE users SET verified = 1 WHERE email = ? '.
                    ' AND verify_string = ? AND verified = 0');
$res = $sth->execute(array($_GET['email'], $_GET['verify_string']));
var_dump($res, $sth->rowCount());
if (! $res) {
    print "Please try again later due to a database error.";
} else {
    if ($sth->rowCount() == 1) {
        print "Thank you, your account is verified.";
    } else {
        print "Sorry, you could not be verified.";
    }
}

Статус проверки пользователя обновляется только в том случае, если адрес электронной почты и проверочная строка соответствуют информации из записи в базе данных, которая еще не прошла проверку. Последняя часть (листинг 8.20) представляет собой небольшую программу для удаления непроверенных пользователей по истечении заданного интервала.
Листинг 8 .20 . delete-user .php

// Подключение к базе данных
$db = new PDO('sqlite:users.db');
$window = '-7 days';
$sth = $db->prepare("DELETE FROM users WHERE verified = 0 AND ".
                    "created_on < datetime('now',?)");
$res = $sth->execute(array($window));
if ($res) {
    print "Deactivated " . $sth->rowCount() . " users.\n";
} else {
    print "Can't delete users.\n";
}

Программа из листинга 8.20 ежедневно запускается для очистки таблицы от непроверенных пользователей. Если вы хотите изменить промежуток времени, в течение которого пользователи должны подтвердить регистрацию, измените переменную $window и обновите текст сообщения в соответствии с новым значением.

Программа: Tiny Wiki

Программа из листинга 8.21 объединяет разные концепции, представленные в этой главе, и реализует полнофункциональную систему вики — сайта, страницы которого могут редактироваться пользователями. Структура программы типична для простых PHP-программ такого типа. В первой части кода определяются различные параметры конфигурации. Затем следует секция if/else, которая решает, что нужно делать (вывести страницу, сохранить правку и т. д.), в зависимости от значений отправленной формы или переменных из URL. Оставшаяся часть программы состоит из функций, вызываемых из секции if/else — функции для вывода заголовка и завершителя страницы, загрузки сохраненного контента страницы и вывода формы редактирования страницы.
Программа Tiny Wiki использует внешнюю библиотеку PHP Markdown Мишеля Фортина (Michel Fortin) для преобразования удобного и компактного синтаксиса Markdown в HTML.
Листинг 8 .21 . Tiny Wiki

<?php
// Регистрация PSR-0-совместимого автозагрузчика классов
spl_autoload_register(function($class){
    require preg_replace('{\\\\|_(?!.*\\\\)}', DIRECTORY_SEPARATOR,
        trim($class, '\\')).'.php';
});
// Markdown используется для разметки текста.
// Библиотека доступна по адресу http://michelf.ca/projects/php-markdown/
use \Michelf\Markdown;
// Каталог, в котором будут храниться страницы вики.
// Проследите за тем, чтобы каталог был доступен для записи веб-серверу.
define('PAGEDIR', dirname(__FILE__) . '/pages');
// Получение имени страницы или использование значения по умолчанию.
$page = isset($_GET['page']) ? $_GET['page'] : 'Home';
// Выбор действия: отображение формы редактирования, сохранение
// формы редактирования или отображение страницы.
// Вывод запрошенной формы редактирования
if (isset($_GET['edit'])) {
    pageHeader($page);
    edit($page);
    pageFooter($page, false);
}
// Сохранение отправленной формы редактирования
else if (isset($_POST['edit'])) {
    file_put_contents(pageToFile($_POST['page']), $_POST['contents']);
    // Перенаправление на обычное представление
    // только что отредактированной страницы.
    header('Location: http://'.$_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] .
           '?page='.urlencode($_POST['page']));
    exit();
}
// Отображение страницы
else {
    pageHeader($page);
    // Если страница существует, вывести ее и завершитель со ссылкой "Edit"
    if (is_readable(pageToFile($page))) {
        // Получить контент страницы из файла, в котором она хранится
        $text = file_get_contents(pageToFile($page));
        // Преобразовать синтаксис Markdown (с использованием
        // библиотеки Markdown, загруженной выше)
        $text = Markdown::defaultTransform($text);
        // Создать ссылку на другие страницы вики
        $text = wikiLinks($text);
        // Отображение страницы
        echo $text;
        // Вывод завершителя
        pageFooter($page, true);
    }
    // Если страница не существует, вывести форму редактирования
    // и завершитель без ссылки "Edit"
    else {
        edit($page, true);
        pageFooter($page, false);
    }
}
// Заголовок страницы - очень простой, только название
// и обычная разметка HTML286 Глава 8 . Основы веб-программирования
function pageheader($page) { ?>
<html>
<head>
<title>Wiki: <?php echo htmlentities($page) ?></title>
</head>
<body>
<h1><?php echo htmlentities($page) ?></h1>
<hr/>
<?php
}
// Завершитель страницы - временная метка последнего изменения,
// необязательная ссылка "Edit" и ссылка для возврата
// к главной странице вики
function pageFooter($page, $displayEditLink) {
    $timestamp = @filemtime(pageToFile($page));
    if ($timestamp) {
        $lastModified = strftime('%c', $timestamp);
    } else {
        $lastModified = 'Never';
    }
    if ($displayEditLink) {
        $editLink = ' - <a href="?page='.urlencode($page).'&edit=true">Edit</a>';
    } else {
        $editLink = '';
    }
?>
<hr/>
<em>Last Modified: <?php echo $lastModified ?></em>
<?php echo $editLink ?> - <a href="<?php echo $_SERVER['SCRIPT_NAME'] ?>">Home</
a>
</body>
</html>
<?php
}
// Отображение формы редактирования. Если страница уже существует,
// включить ее текущий контент в форму
function edit($page, $isNew = false) {
    if ($isNew) {
        $contents = '';
?>
<p><b>This page doesn't exist yet.</b> To create it, enter its contents below
and click the <b>Save</b> button.</p>
    <?php } else {
        $contents = file_get_contents(pageToFile($page));
    }
?>
<form method='post' action='<?php echo htmlentities($_SERVER['SCRIPT_NAME']) ?>'>
<input type='hidden' name='edit' value='true'/>
<input type='hidden' name='page' value='<?php echo htmlentities($page) ?>'/>
<textarea name='contents' rows='20' cols='60'>
<?php echo htmlentities($contents) ?></textarea>
<br/>
<input type='submit' value='Save'/>
</form>
<?php
}
// Генерирование имени файла по содержимому страницы. Вызов md5()
// предотвращает проблемы безопасности из-за нежелательных символов в $page
function pageToFile($page) {
    return PAGEDIR.'/'.md5($page);
}
// Преобразование текста вида [something] в странице в ссылку HTML
// на страницу вики "something"
function wikiLinks($page) {
    if (preg_match_all('/\[([^\]]+?)\]/', $page, $matches, PREG_SET_ORDER)) {
        foreach ($matches as $match) {
            $page = str_replace($match[0], '<a href="'.$_SERVER['SCRIPT_NAME'].
'?page='.urlencode($match[1]).'">'.htmlentities($match[1]).'</a>', $page);
        }
    }
    return $page;
}
Программа: HTTP Range

Программа в листинге 8.22 реализует функциональность, которая позволяет клиенту запросить одну или несколько частей файла. Чаще всего данная возможность используется для возобновления прерванной загрузки файла (например, видеоролика, просмотр которого был прерван). Обычно веб-сервер способен сделать это за вас. Он разбирает заголовок, загружает выбранные части файла и возвращает их браузеру (вместе с необходимыми данными HTTP).
Но если вы продаете мультимедийный контент (например, подкасты или музыку), скорее всего, вы не захотите предоставлять доступ к этим файлам. В противном случае файлы сможет загрузить любой пользователь, знающий URL- адрес, — а вам бы хотелось, чтобы файлы могли быть прочитаны только пользователями, которые эти файлы купили. И по этой причине вы не сможете ограничиться одним веб-сервером, придется использовать PHP.
Рецепт 17.11 показывает, как ограничить файл от прямых обращений, но он работает только с целыми файлами. Программа расширяет возможности этого простого примера и позволяет передавать только части файла, запрашиваемого браузером.
На первый взгляд задача не кажется особо сложной. Тем не менее в спецификации HTTP 1.1 появились некоторые возможности, повышающие сложность: множественные диапазоны (с другим синтаксисом ответов), смещения от конца файла (например, «только последние 1000 байт»), специальные коды статуса и заголовки для недействительных запросов.
Кроме перевода требований спецификации в программный код, эта программа демонстрирует методику чтения и отправки кодов статуса HTTP и заголовков. В нее также интегрированы решения из других рецептов, включая Рецепт 1.6.
Листинг 8 .22 . HTTP Range

// При желании добавьте здесь аутентификацию.
// Файл
$file = __DIR__ . '/numbers.txt';
$content_type = 'text/plain';
// Убедиться в том, что файл доступен для чтения,
// и получить его размер
if (($filelength = filesize($file)) === false) {
    error_log("Problem reading filesize of $file.");
}
// Разобрать заголовок для определения информации,
// необходимой для отправки ответа
if (isset($_SERVER['HTTP_RANGE'])) {
    // В ограничителях игнорируется регистр символов
    if (!preg_match('/bytes=\d*-\d*(,\d*-\d*)*$/i', $_SERVER['HTTP_RANGE'])) {
        error_log("Client requested invalid Range.");
        send_error($filelength);
        exit;
    }
    /*
    Спецификация: "Если клиент запрашивает несколько байтовых
    диапазонов в одном запросе, сервер ДОЛЖЕН вернуть их в порядке их
    следования в запросе."
    */
    $ranges = explode(',',
        substr($_SERVER['HTTP_RANGE'], 6)); // Все после bytes=
    $offsets = array();
    // Извлечение и проверка каждого смещения.
    // Остаются только смещения, прошедшие проверку
    foreach ($ranges as $range) {
        $offset = parse_offset($range, $filelength);
        if ($offset !== false) {
            $offsets[] = $offset;
        }
    }
    /*
    В зависимости от количества запрошенных действительных диапазонов
    ответ должен возвращаться в разных форматах.
    */
    switch (count($offsets)) {
    case 0:
        // Нет действительных диапазонов
        error_log("Client requested no valid ranges.");
        send_error($filelength);
        exit;
        break;
    case 1:
        // Один диапазон, отправить стандартный ответ
        http_response_code(206); // Частичный контент
        list($start, $end) = $offsets[0];
        header("Content-Range: bytes $start-$end/$filelength");
        header("Content-Type: $content_type");
        // Установка переменных, обеспечивающих повторное использование
        // кода между этим и следующим случаем.
        // Примечание: диапазон 0-0 имеет длину 1 байт (границы включаются)
        $content_length = $end - $start + 1;
        $boundaries = array(0 => '', 1 => '');
        break;
    default:
        // Несколько действительных диапазонов, отправить ответ
        // из нескольких частей
        http_response_code(206);  // Частичный контент
        $boundary = str_rand(32); // Строка для разделения частей
        /*
        Необходимо вычислить Content-Length для всего ответа,
        но загрузка полного ответа в строку может привести к большим
        затратам памяти, поэтому значение вычисляется по смещениям.
        Заодно в процессе вычислений будут вычислены границы.
        */
        $boundaries = array();
        $content_length = 0;
       foreach ($offsets as $offset) {
           list($start, $end) = $offset;
               // Используется для разбивки на секции
           $boundary_header =
               "\r\n" .
               "--$boundary\r\n" .
               "Content-Type: $content_type\r\n" .
               "Content-Range: bytes $start-$end/$filelength\r\n" .
               "\r\n";
           $content_length += strlen($boundary_header) + ($end - $start + 1);
           $boundaries[] = $boundary_header;
       }
        // Добавить закрывающую границу
        $boundary_header = "\r\n--$boundary--";
        $content_length += strlen($boundary_header);
        $boundaries[] = $boundary_header;
        // Отсечь лишнюю комбинацию \r\n в первой границе
        $boundaries[0] = substr($boundaries[0], 2);
        $content_length -= 2;
        // Заменить специальным составным значением Content-Type
        $content_type = "multipart/byteranges; boundary=$boundary";
    }
} else {
    // Отправить весь файл.
    // Настроить переменные так, как если бы данные были получены
    // из заголовка Range.
    $start = 0;
    $end = $filelength - 1;
    $offset = array($start, $end);
    $offsets = array($offset);
    $content_length = $filelength;
    $boundaries = array(0 => '', 1 => '');
}
// Сообщить, что будет передаваться.
header("Content-Type: $content_type");
header("Content-Length: $content_length");
// Передача данных
$handle = fopen($file, 'r');
if ($handle) {
    $offsets_count = count($offsets);
// Вывести каждый ограничитель и соответствующую часть файла
    for ($i = 0; $i < $offsets_count; $i++) {
        print $boundaries[$i];
        list($start, $end) = $offsets[$i];
        send_range($handle, $start, $end);
    }
    // Закрывающий ограничитель
    print $boundaries[$i];
    fclose($handle);
}
// Переход к соответствующей позиции в файле
// и вывод запрашиваемой части по фрагментам
function send_range($handle, $start, $end) {
    $line_length = 4096; // "Волшебное" значение
    if (fseek($handle, $start) === -1) {
        error_log("Error: fseek() fail.");
    }
    $left_to_read = $end - $start + 1;
    do {
        $length = min($line_length, $left_to_read);
        if (($buffer = fread($handle, $length)) !== false) {
            print $buffer;
        } else {
            error_log("Error: fread() fail.");
        }
    } while ($left_to_read -= $length);
}
// Отправка заголовка ошибки
function send_error($filelength) {
    http_response_code(416);
    header("Content-Range: bytes */$filelength"); // Необходимо для кода 416
}
// Преобразование смещения в начальную и конечную позиции в файле,
// или возвращение false для недействительного значения
function parse_offset($range, $filelength) {
    /*
    Спецификация: "Значение first-byte-pos в byte-range-spec определяет 
    смещение первого байта в диапазоне".
    Спецификация: "Значение last-byte-pos определяет смещение
    последнего байта в диапазоне; указанные позиции являются включающими".
    */
    list($start, $end) = explode('-', $range);
    /*
    Спецификация: "Значение suffix-byte-range-spec используется
    для определения суффикса тела сущности с длиной, задаваемой
    значением suffix-length".
    */
    if ($start === '') {
        if ($end === '' || $end === 0) {
            // Запрашивается диапазон "-" или "-0"
            return false;
        } else {
            /*
            Спецификация: "Если сущность короче заданного значения
            suffix-length, то используется все тело сущности".
            Спецификация: "Байтовые смещения начинаются с нуля".
            */
            $start = max(0, $filelength - $end);
            $end = $filelength - 1;
        }
    } else {
        /*
        Спецификация: "Если значение last-byte-pos отсутствует или оно 
        больше либо равно текущей длине тела сущности, то значение last-byte-pos
        принимается равным текущей длине тела сущности в байтах,
        уменьшенной на единицу".
        */
        if ($end === '' || $end > $filelength - 1) {
            $end = $filelength - 1;
        }
        /*
        Спецификация: "Если значение last-byte-pos присутствует, то оно
        ДОЛЖНО быть больше либо равно first-byte-pos для этого диапазона
        byte-range-spec, или диапазон byte-range-spec считается
        синтаксически недействительным".
        Проверка также перехватывает случаи start > filelength
        */
        if ($start > $end) {
            return false;
        }
    }
    return array($start, $end);
}
// Генерирование случайной строки для ограничения секций в ответе
function str_rand($length = 32,
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
    if (!is_int($length) || $length < 0) {
        return false;
    }
    $characters_length = strlen($characters) - 1;
    $string = '';
    for ($i = $length; $i > 0; $i--) {
        $string .= $characters[mt_rand(0, $characters_length)];
    }
    return $string;
}

Для простоты демонстрационный файл numbers .txt содержит следующий набор цифр:
01234567890123456789
Следующий пример показывает, как работает частичная передача при выдаче запросов из программы командной строки curl встроенному веб-серверу PHP.
Ниже приведено полное содержимое файла с подробной версией вывода HTTP (без заголовков Range):

$ curl -v http://localhost:8000/range.php
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 8000 (#0)
> GET /range.php HTTP/1.1
> User-Agent: curl/7.24.0
> Host: localhost:8000
> Accept: */*
>
[Sun Aug 18 14:33:36 2013] ::1:59812 [200]: /range.php
< HTTP/1.1 200 OK
< Host: localhost:8000
< Connection: close
< X-Powered-By: PHP/5.4.9
< Content-Type: text/plain
< Content-Length: 10
<
* Closing connection #0
0123456789
Только первые пять байтов:
$ curl -v -H 'Range: bytes=0-4' http://localhost:8000/range.php
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* connected8 .21 . Программа: HTTP Range 293
* Connected to localhost (::1) port 8000 (#0)
> GET /range.php HTTP/1.1
> User-Agent: curl/7.24.0
> Host: localhost:8000
> Accept: */*
> Range: bytes=0-4
>
[Sun Aug 18 14:30:52 2013] ::1:59798 [206]: /range.php
< HTTP/1.1 206 Partial Content
< Host: localhost:8000
< Connection: close
< X-Powered-By: PHP/5.4.9
< Content-Range: bytes 0-4/10
< Content-Type: text/plain
< Content-Length: 5
<
* Closing connection #0
01234

Обратите внимание: на этот раз используется код статуса 206 вместо 200, а заголовок HTTP Content-Range сообщает, какие байты были возвращены.
Последние пять байтов:

$ curl -v -H 'Range: bytes=-5' http://localhost:8000/range.php
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 8000 (#0)
> GET /range.php HTTP/1.1
> User-Agent: curl/7.24.0
> Host: localhost:8000
> Accept: */*
> Range: bytes=-5
>
[Sun Aug 18 14:30:33 2013] ::1:59796 [206]: /range.php
< HTTP/1.1 206 Partial Content
< Host: localhost:8000
< Connection: close
< X-Powered-By: PHP/5.4.9
< Content-Range: bytes 5-9/10
< Content-Type: text/plain
< Content-Length: 5
<
* Closing connection #0
56789

Первые пять и последние пять байтов:

$ curl -v -H 'Range: bytes=0-4,-5' http://localhost:8000/range.php
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 8000 (#0)
> GET /range.php HTTP/1.1
> User-Agent: curl/7.24.0294 Глава 8 . Основы веб-программирования
> Host: localhost:8000
> Accept: */*
> Range: bytes=0-4,-5
>
[Sun Aug 18 14:30:12 2013] ::1:59794 [206]: /range.php
< HTTP/1.1 206 Partial Content
< Host: localhost:8000
< Connection: close
< X-Powered-By: PHP/5.4.9
< Content-Type: multipart/byteranges; boundary=ALLIeNOkvwgKk0ib91ZNph5qi8fHo2ai
< Content-Length: 236
<
--ALLIeNOkvwgKk0ib91ZNph5qi8fHo2ai
Content-Type: text/plain
Content-Range: bytes 0-4/10
01234
--ALLIeNOkvwgKk0ib91ZNph5qi8fHo2ai
Content-Type: text/plain
Content-Range: bytes 5-9/10
56789
* Closing connection #0
--ALLIeNOkvwgKk0ib91ZNph5qi8fHo2ai--

Заголовок Content-Type изменяется со значения text/plain на multipart/ byteranges; boundary=ALLIeNOkvwgKk0ib91ZNph5qi8fHo2ai. «Настоящие» заголовки Content переместились в соответствующие секции.
Так как речь идет о полном содержимом файла, оно может поставляться так, как если бы оно запрашивалось без заголовка Range. Недействительный запрос (байты 20–24 не существуют):

$ curl -v -H 'Range: bytes=20-24' http://localhost:8000/range.php
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 8000 (#0)
> GET /range.php HTTP/1.1
> User-Agent: curl/7.24.0
> Host: localhost:8000
> Accept: */*
> Range: bytes=20-24
>
[Sun Aug 18 14:32:17 2013] Client requested no valid ranges.
[Sun Aug 18 14:32:17 2013] ::1:59806 [416]: /range.php
< HTTP/1.1 416 Requested Range Not Satisfiable
< Host: localhost:8000
< Connection: close
< X-Powered-By: PHP/5.4.9
< Content-Range: bytes */10
< Content-type: text/html
<
* Closing connection #0

На этот раз возвращается третий код статуса 416 с полезным заголовком, который сообщает допустимый диапазон значений для запроса: Content-Range: bytes */10.
Наконец, рассмотрим комбинацию допустимого и недопустимого значения:

$ curl -v -H 'Range: bytes=0-4,20-24' http://localhost:8000/range.php
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 8000 (#0)
> GET /range.php HTTP/1.1
> User-Agent: curl/7.24.0
> Host: localhost:8000
> Accept: */*
> Range: bytes=0-4,20-24
>
[Sun Aug 18 14:31:27 2013] ::1:59801 [206]: /range.php
< HTTP/1.1 206 Partial Content
< Host: localhost:8000
< Connection: close
< X-Powered-By: PHP/5.4.9
< Content-Range: bytes 0-4/10
< Content-Type: text/plain
< Content-Length: 5
<
* Closing connection #0
01234

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

Чтение каналов RSS и Atom

Задача
Требуется загружать каналы RSS и Atom и обрабатывать их содержимое. Это позволит вам интегрировать каналы новостей с нескольких сайтов в ваше приложение.
Решение
Воспользуйтесь парсером MagpieRSS. В следующем примере читается канал RSS для списка рассылки php.announce:

require __DIR__ . '/magpie/rss_fetch.inc';
$feed = 'http://news.php.net/group.php?group=php.announce&format=rss';
$rss = fetch_rss( $feed );
print "<ul>\n";
foreach ($rss->items as $item) {
    print '<li><a href="' . $item['link'] . '">' . $item['title'] .
          "</a></li>\n";
}
print "</ul>\n";
Генерирование каналов RSS

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

class rss2 extends DOMDocument {
    private $channel;
    public function __construct($title, $link, $description) {
        parent::__construct();
        $this->formatOutput = true;
        $root = $this->appendChild($this->createElement('rss'));
        $root->setAttribute('version', '2.0');
        $channel= $root->appendChild($this->createElement('channel'));
        $channel->appendChild($this->createElement('title', $title));
        $channel->appendChild($this->createElement('link', $link));
        $channel->appendChild($this->createElement('description',
                                                   $description));
        $this->channel = $channel;
    }
    public function addItem($title, $link, $description) {
        $item = $this->createElement('item');
        $item->appendChild($this->createElement('title', $title));
        $item->appendChild($this->createElement('link', $link));
        $item->appendChild($this->createElement('description', $description));
        $this->channel->appendChild($item);
    }
}
$rss = new rss2('Channel Title', 'http://www.example.org',
                'Channel Description');
$rss->addItem('Item 1', 'http://www.example.org/item1',
              'Item 1 Description');
$rss->addItem('Item 2', 'http://www.example.org/item2',
              'Item 2 Description');
print $rss->saveXML();
Генерирование каналов Atom

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

class atom1 extends DOMDocument {
    private $ns;
    public function __construct($title, $href, $name, $id) {
        parent::__construct();
        $this->formatOutput = true;
        $this->ns = 'http://www.w3.org/2005/Atom';
        $root = $this->appendChild($this->createElementNS($this->ns, 'feed'));
        $root->appendChild($this->createElementNS($this->ns, 'title', $title));
        $link = $root->appendChild($this->createElementNS($this->ns, 'link'));
        $link->setAttribute('href', $href);
        $root->appendChild($this->createElementNS($this->ns, 'updated',
            date(DATE_ATOM)));
        $author = $root->appendChild($this->createElementNS($this->ns,
                                                            'author'));
        $author->appendChild($this->createElementNS($this->ns, 'name', $name));
        $root->appendChild($this->createElementNS($this->ns, 'id', $id));
    }
    public function addEntry($title, $link, $summary) {
        $entry = $this->createElementNS($this->ns, 'entry');
        $entry->appendChild($this->createElementNS($this->ns, 'title', $title));
        $entry->appendChild($this->createElementNS($this->ns, 'link', $link));
        $id = uniqid('http://example.org/atom/entry/ids/');
        $entry->appendChild($this->createElementNS($this->ns, 'id', $id));
        $entry->appendChild($this->createElementNS($this->ns, 'updated',
            date(DATE_ATOM)));
        $entry->appendChild($this->createElementNS($this->ns, 'summary',
            $summary));
        $this->documentElement->appendChild($entry);
    }
}
$atom = new atom1('Channel Title', 'http://www.example.org',
                'John Quincy Atom', 'http://example.org/atom/feed/ids/1');12 .14 . Генерирование каналов Atom 427
$atom->addEntry('Item 1', 'http://www.example.org/item1',
              'Item 1 Description', 'http://example.org/atom/entry/ids/1');
$atom->addEntry('Item 2', 'http://www.example.org/item2',
              'Item 2 Description', 'http://example.org/atom/entry/ids/2');
print $atom->saveXML();
Пометки в веб-страницах

Задача
Требуется вывести веб-страницу (например, результаты поиска) с цветовым выделением некоторых слов.
Решение
Постройте массив с заменой для каждого выделяемого слова. Затем разбейте страницу на «элементы HTML» и «текст между элементами HTML» и примените замену только к тексту между элементами HTML. В листинге 13.1 цветовое выделение слов, находящихся в $words, применяется к разметке HTML из $body. Листинг 13 .1 . Разметка веб-страницы

$body = '
<p>I like pickles and herring.</p>
<a href="pickle.php"><img src="pickle.jpg"/>A pickle picture</a>
I have a herringbone-patterned toaster cozy.
<herring>Herring is not a real HTML element!</herring>
';
$words = array('pickle','herring');
$replacements = array();
foreach ($words as $i => $word) {
    $replacements[] = "<span class='word-$i'>$word</span>";
}
// Страница разбивается по разделителям, которые
// приближенно определяются как элементы HTML.
$parts = preg_split("{(<(?:\"[^\"]*\"|'[^']*'|[^'\">])*>)}",
                    $body,
                    -1,  // Количество фрагментов не ограничивается
                    PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $i => $part) {
    // Пропустить, если фрагмент является элементом HTML
    if (isset($part[0]) && ($part[0] == '<')) { continue; }
    // Заключить слова в теги <span/>
    $parts[$i] = str_replace($words, $replacements, $part);
}
// Восстановление тела страницы
$body = implode('',$parts);
print $body;

Чтобы поиск выполнялся без учета регистра, следует переключиться с str_ replace() на регулярные выражения (использовать str_ireplace() не удастся, потому что замена должна сохранять регистр совпавшей части). В листинге 13.2 приведен измененный код, использующий регулярные выражения для выполне- ния замены. Листинг 13 .2 . Разметка веб-страницы с использованием регулярных выражений

$body = '
<p>I like pickles and herring.</p>
<a href="pickle.php"><img src="pickle.jpg"/>A pickle picture</a>
I have a herringbone-patterned toaster cozy.
<herring>Herring is not a real HTML element!</herring>
';
$words = array('pickle','herring');
$patterns = array();
$replacements = array();
foreach ($words as $i => $word) {
    $patterns[] = '/\b' . preg_quote($word) .'\b/i';
    $replacements[] = "<span class='word-$i'>\\0</span>";
}
// Страница разбивается по разделителям, которые
// приближенно определяются как элементы HTML.
$parts = preg_split("{(<(?:\"[^\"]*\"|'[^']*'|[^'\">])*>)}",
                    $body,
                    -1,  // Количество фрагментов не ограничивается
                    PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $i => $part) {
    // Пропустить, если фрагмент является элементом HTML
    if (isset($part[0]) && ($part[0] == '<')) { continue; }
    // Заключить слова в теги <span/>
    $parts[$i] = preg_replace($patterns, $replacements, $part);
}
// Восстановление тела страницы
$body = implode('',$parts);
print $body;
Извлечение ссылок из файлов HTML

Задача
Требуется извлечь URL-адреса, указанные в документе HTML.
Решение
Воспользуйтесь Tidy для преобразования документа в XHTML, а затем примените запрос XPath для поиска всех ссылок, как показано в листинге 13.6.
Листинг 13 .6 . Извлечение ссылок с использованием Tidy и XPath

$html=<<<_HTML_
<p>Some things I enjoy eating are:</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Pickle">Pickles</a></li>
<li><a href="http://www.eatingintranslation.com/2011/03/great_ny_noodle.html">
                 Salt-Baked Scallops</a></li>
<li><a href="http://www.thestoryofchocolate.com/">Chocolate</a></li>
</ul>
_HTML_;
$doc = new DOMDocument();
$opts = array('output-xhtml' => true,
              // Предотвратить путаницу с сущностями
              'numeric-entities' => true);
$doc->loadXML(tidy_repair_string($html,$opts));
$xpath = new DOMXPath($doc);
// Сообщить $xpath о пространстве имен XHTML
$xpath->registerNamespace('xhtml','http://www.w3.org/1999/xhtml');
foreach ($xpath->query('//xhtml:a/@href') as $node) {
    $link = $node->nodeValue;
    print $link . "\n";
}

Если расширение Tidy недоступно, воспользуйтесь функцией pc_link_extractor(), приведенной в листинге 13.7.
Листинг 13 .7 . Извлечение ссылок без использования Tidy


$html=<<<_HTML_
<p>Some things I enjoy eating are:</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Pickle">Pickles</a></li>
<li><a href="http://www.eatingintranslation.com/2011/03/great_ny_noodle.html">
                 Salt-Baked Scallops</a></li>
<li><a href="http://www.thestoryofchocolate.com/">Chocolate</a></li>
</ul>
_HTML_;
$links = pc_link_extractor($html);
foreach ($links as $link) {
    print $link[0] . "\n";
}
function pc_link_extractor($html) {
    $links = array();
    preg_match_all('/<a\s+.*?href=[\"\']?([^\"\' >]*)[\"\']?[^>]*>(.*?)<\/a>/i',
                   $html,$matches,PREG_SET_ORDER);
    foreach($matches as $match) {438 Глава 13 . Автоматизация в веб-приложениях
        $links[] = array($match[1],$match[2]);
    }
    return $links;
}

Выражение XPath в листинге 13.6 получает только ссылки, но не якоря. В листинге 13.8 продемонстрировано альтернативное решение, которое выдает как ссылки, так и якоря.
Листинг 13 .8 . Извлечение ссылок и якорей с использованием Tidy и XPath

$html=<<<_HTML_
<p>Some things I enjoy eating are:</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Pickle">Pickles</a></li>
<li><a href="http://www.eatingintranslation.com/2011/03/great_ny_noodle.html">
                 Salt-Baked Scallops</a></li>
<li><a href="http://www.thestoryofchocolate.com/">Chocolate</a></li>
</ul>
_HTML_;13 .4 . Преобразование простого текста в HTML 439
$doc = new DOMDocument();
$opts = array('output-xhtml'=>true,
              'wrap' => 0,
              // Предотвратить путаницу с сущностями
              'numeric-entities' => true);
$doc->loadXML(tidy_repair_string($html,$opts));
$xpath = new DOMXPath($doc);
// Сообщить $xpath о пространстве имен XHTML
$xpath->registerNamespace('xhtml','http://www.w3.org/1999/xhtml');
foreach ($xpath->query('//xhtml:a') as $node) {
    $anchor = trim($node->textContent);
    $link = $node->getAttribute('href');
    print "$anchor -> $link\n";
}
Преобразование простого текста в HTML

Задача
Требуется преобразовать простой текст в разметку HTML с осмысленным форматированием.
Решение
Начните с кодирования сущностей вызовом htmlentities(). Затем преобразуйте текст в различные структуры HTML. Функция pc_text2html() из листинга 13.9 содержит базовые преобразования для ссылок и разрывов абзацев.
Листинг 13 .9 . pc_text2html()

function pc_text2html($s) {
  $s = htmlentities($s);
  $grafs = split("\n\n",$s);
  for ($i = 0, $j = count($grafs); $i < $j; $i++) {
    // Ссылка на URL-адреса http или ftp
    $grafs[$i] = preg_replace('/((ht|f)tp:\/\/[^\s&]+)/',
                              '<a href="$1">$1</a>',$grafs[$i]);
    // Ссылка на адрес электронной почты
    $grafs[$i] = preg_replace('/[^@\s]+@([-a-z0-9]+\.)+[a-z]{2,}/i',
        '<a href="mailto:$1">$1</a>',$grafs[$i]);
    // Начать с нового абзаца
    $grafs[$i] = '<p>'.$grafs[$i].'</p>';
  }
  return implode("\n\n",$grafs);
}

Чем больше вы знаете о том, что собой представляет преобразуемый текст, тем лучше пройдет преобразование. Например, если выделение обозначается звездочками (*) или символами косой черты (/), в которые заключаются слова, в функцию можно добавить соответствующие правила, как показано в листинге 13.10. Листинг 13 .10 . pc_text2html()

$grafs[$i] = preg_replace('/(\A|\s)\*([^*]+)\*(\s|\z)/',
                          '$1<b>$2</b>$3',$grafs[$i]);
$grafs[$i] = preg_replace('{(\A|\s)/([^/]+)/(\s|\z)}',
                          '$1<i>$2</i>$3',$grafs[$i]);
Преобразование HTML в простой текст

Задача
Требуется преобразовать разметку HTML в удобочитаемый отформатированный текст.
Решение
Воспользуйтесь классом html2text. В листинге 13.11 представлен пример его использования.
Листинг 13 .11 . Преобразование HTML в простой текст

require_once 'class.html2text.inc';
/* Функции file_get_contents() передается путь или URL-адрес
   обрабатываемого файла HTML */
$html = file_get_contents(__DIR__ . '/article.html');
$converter = new html2text($html);
$plain_text = $converter->get_text();
Программа: поиск устаревших ссылок

Программа stale-links .php в листинге 13.22 строит список ссылок в странице и выводит информацию об их состоянии (рабочая ссылка, нерабочая ссылка, ссылка перемещена). При запуске программе передается URL-адрес для проверки ссылок:

http://oreilly.com: OK
https://members.oreilly.com: MOVED: https://members.oreilly.com/account/login
http://shop.oreilly.com/basket.do: OK
http://shop.oreilly.com: OK
http://radar.oreilly.com: OK
http://animals.oreilly.com: OK
http://programming.oreilly.com: OK
...

Программа stale-links .php использует расширение cURL для получения вебстраниц (см. листинг 13.22). Сначала она загружает URL-адрес, указанный в командной строке. Когда страница будет загружена, программа использует средства XPath из Рецепта 13.3 для получения списка ссылок на странице. Затем после включения в начало каждой ссылки базового URL-адреса (если это необходимо) программа обращается по ссылке. Так как нас интересуют только заголовки ответов, вместо GET используется метод HEAD, для чего устанавливается режим CURLOPT_NOBODY. Установка режима CURLOPT_HEADER приказывает curl_exec() включить заголовки ответа в возвращаемую строку. В зависимости от кода ответа выводится статус ссылки и ее новое местонахождение в случае перемещения.
Листинг 13 .22 . stale-links .php

if (! isset($_SERVER['argv'][1])) {
    die("No URL provided.\n");
}
$url = $_SERVER['argv'][1];
// Загрузка страницы
list($page,$pageInfo) = load_with_curl($url);
if (! strlen($page)) {
    die("No page retrieved from $url");
}
// Преобразование в XML для простоты разбора
$opts = array('output-xhtml' => true,
              'numeric-entities' => true);
$xml = tidy_repair_string($page, $opts);
$doc = new DOMDocument();
$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('xhtml','http://www.w3.org/1999/xhtml');
// Вычисление базового URL-адреса для относительных ссылок
$baseURL = '';
// Проверить наличие в странице <base href=""/>
$nodeList = $xpath->query('//xhtml:base/@href');
if ($nodeList->length == 1) {
    $baseURL = $nodeList->item(0)->nodeValue;
}
// Тег <base href=""/> отсутствует, построить базовый URL на основе $url
else {
    $URLParts = parse_url($pageInfo['url']);
    if (! (isset($URLParts['path']) && strlen($URLParts['path']))) {
        $basePath = '';
    } else {
        $basePath = preg_replace('#/[^/]*$#','',$URLParts['path']);
    }
    if (isset($URLParts['username']) || isset($URLParts['password'])) {
        $auth = isset($URLParts['username']) ? $URLParts['username'] : '';
        $auth .= ':';
        $auth .= isset($URLParts['password']) ? $URLParts['password'] : '';
        $auth .= '@';
    } else {
        $auth = '';
    }
    $baseURL = $URLParts['scheme'] . '://' .
               $auth . $URLParts['host'] .
               $basePath;
}
// Сохранение всех посещенных ссылок, чтобы каждая ссылка
// посещалась не более одного раза
$seenLinks = array();
// Получение всех ссылок
$links = $xpath->query('//xhtml:a/@href');
foreach ($links as $node) {
    $link = $node->nodeValue;
    // Разрешение относительных ссылок
    if (! preg_match('#^(http|https|mailto):#', $link)) {
        if (((strlen($link) == 0)) || ($link[0] != '/')) {
            $link = '/' . $link;
        }
        $link = $baseURL . $link;
    }
    // Если ссылка встречалась ранее, она пропускается
    if (isset($seenLinks[$link])) {
        continue;
    }
    // Ссылка помечается как посещенная
    $seenLinks[$link] = true;
    // Вывод посещенной ссылки
    print $link.': ';
    flush();
    list($linkHeaders, $linkInfo) = load_with_curl($link, 'HEAD');
    // Дальнейшие действия определяются кодом ответа.
    // Коды 2xx означают, что со страницей все нормально
    if (($linkInfo['http_code'] >= 200) && ($linkInfo['http_code'] < 300)) {
        $status = 'OK';
    }
    // Коды 3xx - признак перенаправления
    else if (($linkInfo['http_code'] >= 300) && ($linkInfo['http_code'] < 400)) {
        $status = 'MOVED';
        if (preg_match('/^Location: (.*)$/m',$linkHeaders,$match)) {
                $status .= ': ' . trim($match[1]);
        }
    }
    // Другие коды ответов - признак ошибки
    else {
        $status = "ERROR: {$linkInfo['http_code']}";
    }
    // Вывод информации о ссылке
    print "$status\n";
}
function load_with_curl($url, $method = 'GET') {
    $c = curl_init($url);
    curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
    if ($method == 'GET') {
        curl_setopt($c,CURLOPT_FOLLOWLOCATION, true);
    }452 Глава 13 . Автоматизация в веб-приложениях
    else if ($method == 'HEAD') {
        curl_setopt($c, CURLOPT_NOBODY, true);
        curl_setopt($c, CURLOPT_HEADER, true);
    }
    $response = curl_exec($c);
    return array($response, curl_getinfo($c));
}
Программа: проверка актуальности ссылок

Листинг 13.23 представляет собой модификацию программы в листинге 13.22, которая выводит список ссылок и время их последнего изменения. Если сервер, на котором находится URL, не предоставляет информацию о времени последнего изменения, программа выводит время запроса URL-адреса. Если программе не удалось успешно загрузить данные по URL-адресу, она выводит код статуса, полученный при попытке обращения. При запуске программе передается URL- адрес для проверки ссылок:

http://oreilly.com: OK; Last Modified: Fri, 24 May 2013 18:09:11 GMT
https://members.oreilly.com: MOVED: https://members.oreilly.com/account/login
http://shop.oreilly.com/basket.do: OK
http://shop.oreilly.com: OK
http://radar.oreilly.com: OK; Last Modified: Fri, 24 May 2013 20:40:56 GMT
http://animals.oreilly.com: OK; Last Modified: Fri, 24 May 2013 20:40:18 GMT
http://programming.oreilly.com: OK; Last Modified: Fri, 24 May 2013 20:42:44 GMT
...

Приведенные результаты были получены при запуске программы около 20:43 (GMT) 24 мая 2013 года. Если за ссылкой не указывается время последнего изменения, это означает, что сервер не предоставил соответствующую информацию, так что страница, вероятно, является динамической.
Программа проверки актуальности ссылок концептуально почти не отличается от программы поиска устаревших ссылок. Она использует те же средства для извлечения ссылок и страницы и тот же код для загрузки URL.
После того как страница будет загружена, URL-адрес каждой ссылки загружается методом head. Однако вместо того, чтобы просто выводить новое местонахождение перемещенных ссылок, программа выводит отформатированную версию заголовка Last-Modified (если она доступна).
Листинг 13 .23 . fresh-links .php

error_reporting(E_ALL);
if (! isset($_SERVER['argv'][1])) {
    die("No URL provided.\n");
}
$url = $_SERVER['argv'][1];
// Загрузка страницы
list($page, $pageInfo) = load_with_curl($url);
if (! strlen($page)) {
    die("No page retrieved from $url");
}
// Преобразование в XML для простоты разбора
$opts = array('output-xhtml' => true,
              'numeric-entities' => true);
$xml = tidy_repair_string($page, $opts);
$doc = new DOMDocument();
$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('xhtml','http://www.w3.org/1999/xhtml');
// Вычисление базового URL-адреса для относительных ссылок
$baseURL = '';
// Проверить наличие в странице <base href=""/>
$nodeList = $xpath->query('//xhtml:base/@href');
if ($nodeList->length == 1) {
    $baseURL = $nodeList->item(0)->nodeValue;
}
// Тег <base href=""/> отсутствует, построить базовый URL на основе $url
else {
    $URLParts = parse_url($pageInfo['url']);
    if (! (isset($URLParts['path']) && strlen($URLParts['path']))) {
        $basePath = '';
    } else {
        $basePath = preg_replace('#/[^/]*$#','',$URLParts['path']);
    }
    if (isset($URLParts['username']) || isset($URLParts['password'])) {
        $auth = isset($URLParts['username']) ? $URLParts['username'] : '';
        $auth .= ':';
        $auth .= isset($URLParts['password']) ? $URLParts['password'] : '';
        $auth .= '@';
    } else {
        $auth = '';
    }
    $baseURL = $URLParts['scheme'] . '://' .
               $auth . $URLParts['host'] .
               $basePath;
}
// Сохранение всех посещенных ссылок, чтобы каждая ссылка
// посещалась не более одного раза
$seenLinks = array();
// Получение всех ссылок
$links = $xpath->query('//xhtml:a/@href');
foreach ($links as $node) {
    $link = $node->nodeValue;
    // Разрешение относительных ссылок
    if (! preg_match('#^(http|https|mailto):#', $link)) {
        if (((strlen($link) == 0)) || ($link[0] != '/')) {
            $link = '/' . $link;
        }
        $link = $baseURL . $link;
    }
    // Если ссылка встречалась ранее, она пропускается
    if (isset($seenLinks[$link])) {
        continue;
    }
    // Ссылка помечается как посещенная
    $seenLinks[$link] = true;
    // Вывод посещенной ссылки
    print $link.': ';
    flush();
    list ($linkHeaders, $linkInfo) = load_with_curl($link, 'HEAD');
    // Дальнейшие действия определяются кодом ответа.
    // Коды 2xx означают, что со страницей все нормально
    if (($linkInfo['http_code'] >= 200) && ($linkInfo['http_code'] < 300)) {
        $status = 'OK';
    }
    // Коды 3xx - признак перенаправления
    else if (($linkInfo['http_code'] >= 300) && ($linkInfo['http_code'] < 400)) {
        $status = 'MOVED';
        if (preg_match('/^Location: (.*)$/m',$linkHeaders,$match)) {
                $status .= ': ' . trim($match[1]);
        }
    }
    // Другие коды ответов - признак ошибки
    else {
        $status = "ERROR: {$linkInfo['http_code']}";
    }
    if (preg_match('/^Last-Modified: (.*)$/mi', $linkHeaders, $match)) {
        $status .= "; Last Modified: " . trim($match[1]);
    }
    // вывод информации о ссылке
    print "$status\n";
}
function load_with_curl($url, $method = 'GET') {
    $c = curl_init($url);
    curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
    if ($method == 'GET') {
        curl_setopt($c,CURLOPT_FOLLOWLOCATION, true);
    }
    else if ($method == 'HEAD') {
        curl_setopt($c, CURLOPT_NOBODY, true);
        curl_setopt($c, CURLOPT_HEADER, true);
    }
    $response = curl_exec($c);
    return array($response, curl_getinfo($c));
}
Выполнение поиска DNS

Задача
Требуется найти доменное имя по IP-адресу или наоборот.
Решение
Воспользуйтесь функцией gethostbyname() или gethostbyaddr():

$ip   = gethostbyname('www.example.com'); // 93.184.216.119
$host = gethostbyaddr('93.184.216.119'); // www.example.com

Иногда одно имя хоста может отображаться на несколько IP-адресов. Чтобы найти все хосты, используйте функцию gethostbynamel():

$hosts = gethostbynamel('www.yahoo.com');
print_r($hosts);

Также возможно выполнение более сложных задач, связанных с DNS. Например, функция getmxrr() позволяет получать записи MX: getmxrr('yahoo.com', $hosts, $weight);

for ($i = 0; $i < count($hosts); $i++) {
    echo "$weight[$i] $hosts[$i]\n";
}

Если функция gethostbyname() получает A-записи IPv4, а функция getmxrr() получает MX-записи, функция dns_get_record() получает запись DNS указанного типа. Например, эта возможность может пригодиться для получения AAAA- записей IPv6:

$addrs = dns_get_record('www.yahoo.com', DNS_AAAA);
print_r($addrs);
Проверка доступности хоста

Задача
Требуется проверить связь с хостом. Это позволит вам определить, работает ли хост и доступен ли он с вашего компьютера.
Решение
Воспользуйтесь пакетом PEAR Net_Ping:

require 'Net/Ping.php';
$ping = Net_Ping::factory();
if ($ping->checkHost('www.oreilly.com')) {
    print 'Reachable';
} else {
    print 'Unreachable';
}
$data = $ping->ping('www.oreilly.com');
Получение информации о доменном имени

Задача
Требуется получить контактную информацию или другие сведения о доменном имени.
Решение
Воспользуйтесь классом PEAR Net_Whois:

require 'Net/Whois.php';
$server = 'whois.godaddy.com';16 .9 . Получение информации о доменном имени 523
$query  = 'oreilly.com';
$whois = new Net_Whois();
$data = $whois->query($query, $server);

Разные домены используют разные серверы Whois, а разные серверы Whois возвращают результаты в разных форматах. Чтобы узнать правильный сервер Whois для домена, начните с выдачи запроса к whois.iana.org. Результат, полученный от сервера, будет содержать строку, начинающуюся с whois:; она обозначает сервер, который следует использовать для верхнего уровня интересующего вас домена.
После этого можно обратиться к указанному серверу за информацией о домене:

require 'Net/Whois.php';
$query  = 'oreilly.com';
$iana_server = 'whois.iana.org';
$whois = new Net_Whois();524 Глава 16 . Сервисы Интернета
$iana_data = $whois->query($query, $iana_server);
preg_match('/^whois:\s+(.+)$/m', $iana_data, $matches);
$tld_whois_server = $matches[1];
$tld_data = $whois->query($query, $tld_whois_server);
print $tld_data;

Затем в зависимости от полученной информации может последовать запрос к дополнительному серверу. Например, из второго запроса в приведенном коде следует, что ответственным сервером Whois для oreilly.com является сервер whois. godaddy.com.

Вывод текста как графического изображения

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

ImageString($image, 1, $x, $y, 'I love PHP Cookbook', $text_color);

Для шрифтов TrueType используется функция ImageFTText():

ImageFTText($image, $size, 0, $x, $y, $text_color, '/path/to/font.ttf',
             'I love PHP Cookbook');
ImageFTText($image, $size, 0, $x, $y, $text_color, '/path/to/font.ttf',
             'I love PHP Cookbook');
ImageFTText($image, $size, 0, $x, $y, $text_color, '/path/to/font.ttf',
             'I love PHP Cookbook');

Функция ImageString() поддерживает пять вариантов размера шрифта, от 1 до 5.
Номер 1 соответствует самому мелкому шрифту, а номер 5 — самому крупному. Для любых значений за пределами этого диапазона генерируется размер, эквивалентный ближайшему допустимому значению.
Чтобы текст выводился по вертикали, а не по горизонтали, воспользуйтесь функцией ImageStringUp():

ImageStringUp($image, 1, $x, $y, 'I love PHP Cookbook', $text_color);
Наложение водяных знаков

Задача
Требуется наложить водяной знак на изображение.
Решение
Если водяной знак имеет прозрачный фон, вызовите функцию ImageCopy() для использования альфа-канала:

$image = ImageCreateFromPNG('/path/to/image.png');
$stamp = ImageCreateFromPNG('/path/to/stamp.png');
$margin = ['right' => 10, 'bottom' => 10]; // Смещение от края
ImageCopy($image, $stamp,
    imagesx($image) - imagesx($stamp) - $margin['right'],
    imagesy($image) - imagesy($stamp) - $margin['bottom'],
    0, 0, imagesx($stamp), imagesy($stamp));

В противном случае вызовите ImageCopyMerge() с дополнительным параметром — уровнем непрозрачности:

$image = ImageCreateFromPNG('/path/to/image.png');
$stamp = ImageCreateFromPNG('/path/to/stamp.png');
$margin = ['right' => 10, 'bottom' => 10]; // Смещение от края
$opacity = 50; // От 0 до 100%
ImageCopyMerge($image, $stamp,
    imagesx($image) - imagesx($stamp) - $margin['right'],
    imagesy($image) - imagesy($stamp) - $margin['bottom'],
    0, 0, imagesx($stamp), imagesy($stamp),
    $opacity);

Следующий фрагмент генерирует изображение с водяным знаком:

$image = ImageCreateFromJPEG(__DIR__ . '/iguana.jpg');
// Водяной знак
$w = 400; $h = 75;
$stamp = ImageCreateTrueColor($w, $h);
ImageFilledRectangle($stamp, 0, 0, $w-1, $h-1, 0xFFFFFF);
// Сопроводительный текст17 .8 . Наложение водяных знаков 545
$color = 0x000000; // Черный
ImageString($stamp, 4, 10, 10,
    'Galapagos Land Iguana by Nicolas de Camaret', $color);
ImageString($stamp, 4, 10, 28,
    'http://flic.kr/ndecam/6215259398', $color);
ImageString($stamp, 2, 10, 46,
    'Licence at http://creativecommons.org/licenses/by/2.0.', $color);
// Добавление водяного знака
$margin = ['right' => 10, 'bottom' => 10]; // Смещение от края
$opacity = 50; // between 0 and 100%
ImageCopyMerge($image, $stamp,
    imagesx($image) - imagesx($stamp) - $margin['right'],
    imagesy($image) - imagesy($stamp) - $margin['bottom'],
    0, 0, imagesx($stamp), imagesy($stamp),
    $opacity);
// Отправка
header('Content-type: image/png');
ImagePNG($image);
ImageDestroy($image);
ImageDestroy($stamp);
Чтение данных EXIF

Задача
Требуется извлечь метаданные из файла изображения. По этим данным можно узнать, когда была сделана фотография, размер изображения и тип MIME.
Решение
Воспользуйтесь функцией exif_read_data():

$exif = exif_read_data('beth-and-seth.jpeg');
print_r($exif);
Array
(
    [FileName] => beth-and-seth.jpg
    [FileDateTime] => 1096055414
    [FileSize] => 182080
    [FileType] => 2
    [MimeType] => image/jpeg
    [SectionsFound] => APP12
    [COMPUTED] => Array
        (
            [html] => width="642" height="855"
            [Height] => 855
            [Width] => 642
            [IsColor] => 1
        )
    [Company] => Ducky
    [Info] =>
)
Защита изображений

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

header('Content-Type: image/png');
readfile('/path/to/graphic.png');

Впрочем, типичный способ не всегда оказывается лучшим. Во-первых, что делать, если вы хотите ограничить состав файлов, которые могут просматриваться пользователями, но осложняя жизнь пользователей вводом имен и паролей? Одно из возможных решений — создавать ссылки только на существующие файлы; если пользователь не может щелкнуть на ссылке, он не сможет просмотреть файл.
Впрочем, пользователь может создать закладки для старых файлов или попытаться угадать другие имена файлов на основании вашей схемы назначения имен и ввести URL-адрес вручную в браузере.
Чтобы доступ к контенту был действительно ограничен, пользователи не должны иметь возможность угадать имя и просматривать изображения. Обычно в таких ситуациях ограниченной группе пользователей (чаще всего репортерам) предоставляется версия для предварительного ознакомления, чтобы они могли написать материалы по теме или были готовы распространять их в момент снятия ограничений. Теоретически проблему можно решить, позаботившись о том, что в корневом каталоге документов хранится только допустимый контент, но для этого потребуются многочисленные «переброски» файлов между каталогами.
Лучше действовать иначе — хранить все файлы в одном фиксированном месте и предоставлять только файлы, прошедшие проверку в коде.
Допустим, вы заключили контракт с издательством на распространение одного из его комиксов на вашем сайте. Тем не менее издательство не хочет, чтобы вы создавали виртуальный архив, и поэтому вашим пользователям разрешается просматривать комиксы только за последние две недели, а за остальными комиксами они должны обращаться на официальный сайт. Кроме того, вы получаете комиксы до их официального выпуска, но не хотите позволять другим пользователям бесплатно просматривать их; пользователи должны приходить на ваш сайт ежедневно.
Решение приведено ниже. Поступающие файлы снабжаются временной меткой, поэтому вы можете легко определить, какой файл к какому дню относится. Для блокировки комиксов за пределами 14-дневного окна используется следующий код:

// Просмотр комикса разрешен, если его временная метка
// находится в допустимых границах
// Вычисление текущей даты
list($now_m,$now_d,$now_y) = explode(',',date('m,d,Y'));
$now = mktime(0,0,0,$now_m,$now_d,$now_y);
// Двухчасовые допуски с обеих сторон учитывают возможное действие
// летнего времени
$min_ok = $now - 14*86400 - 7200; // 14 дней назад
$max_ok = $now + 7200;            // Сейчас
$mo = (int) $_GET['mo'];
$dy = (int) $_GET['dy'];
$yr = (int) $_GET['yr'];
// Определение временной метки запрашиваемого комикса
$asked_for = mktime(0,0,0,$mo,$dy,$yr);
// Сравнение дат
if (($min_ok > $asked_for) || ($max_ok < $asked_for)) {
    echo 'You are not allowed to view the comic for that day.';
} else {
    header('Content-type: image/png');
    readfile("/www/comics/{$mo}{$dy}{$yr}.png");
}
Программа: генерирование гистограммы по результатам опроса

При выводе результатов опроса разноцветная гистограмма часто оказывается нагляднее простого вывода текста. Функция, приведенная в листинге 17.1, использует GD для создания изображения, отображающего накопленные результаты опроса.
Листинг 17 .1 . Построение гистограммы

function bar_chart($question, $answers) {
    // Определение цветов для рисования полос
    $colors = array(0xFF6600, 0x009900, 0x3333CC,
        0xFF0033, 0xFFFF00, 0x66FFFF, 0x9900CC);
    $total = array_sum($answers['votes']);
    // Определение интервалов и других служебных констант
    $padding = 5;
    $line_width = 20;
    $scale = $line_width * 7.5;
    $bar_height = 10;
    $x = $y = $padding;
    // Для рисования создается большое изображение,
    // поскольку длина изображения неизвестна заранее
    $image = ImageCreateTrueColor(150, 500);
    ImageFilledRectangle($image, 0, 0, 149, 499, 0xE0E0E0);
    $black = 0x000000;
    // Вывод вопроса
    $wrapped = explode("\n", wordwrap($question, $line_width));
    foreach ($wrapped as $line) {
        ImageString($image, 3, $x, $y , $line, $black);
        $y += 12;
    }
    $y += $padding;
    // Вывод ответов
    for ($i = 0; $i < count($answers['answer']); $i++) {
        // Форматирование процента
        $percent = sprintf('%1.1f', 100*$answers['votes'][$i]/$total);
        $bar = sprintf('%d', $scale*$answers['votes'][$i]/$total);
        // Определение цвета
        $c = $i % count($colors); // Если полос больше, чем цветов
        $text_color = $colors[$c];
        // Рисование столбца и процента ответов
        ImageFilledRectangle($image, $x, $y, $x + $bar,554 Глава 17 . Графика
                             $y + $bar_height, $text_color);
        ImageString($image, 3, $x + $bar + $padding, $y,
                    "$percent%", $black);
         $y += 12;
         // Вывод ответа
         $wrapped = explode("\n", wordwrap($answers['answer'][$i], $line_width));
         foreach ($wrapped as $line) {
             ImageString($image, 2, $x, $y, $line, $black);
             $y += 12;
         }
         $y += 7;
     }
     // Изображение обрезается при копировании
     $chart = ImageCreateTrueColor(150, $y);
     ImageCopy($chart, $image, 0, 0, 0, 0, 150, $y);
     // Для PHP 5.5+ 
     // $chart = ImageCrop($image, array('x' => 0, 'y' => 0,
     //                                  'width' => 150, 'height' => $y));
     // Поставка изображения
     header ('Content-type: image/png');
     ImagePNG($chart);
     // Освобождение ресурсов
     ImageDestroy($image);
     ImageDestroy($chart);
}

Чтобы вызвать эту программу, создайте массив, содержащий два параллельных массива: $answers['answer'] и $answer['votes'].
Элемент $i каждого массива содержит текст ответа и общее количество голосов, поданных за ответ $i.
На рис. 17.14 представлен примерный результат:

// Акт II, сцена II.
$question = 'What a piece of work is man?';
$answers['answer'][] = 'Noble in reason';
$answers['votes'][]  = 29;
$answers['answer'][] = 'Infinite in faculty';
$answers['votes'][]  = 22;
$answers['answer'][] = 'In form, in moving, how express and admirable';
$answers['votes'][]  = 59;
$answers['answer'][] = 'In action how like an angel';
$answers['votes'][]  = 45;
bar_chart($question, $answers);

В нашем примере ответы были присвоены вручную, но в реальном опросе информация может извлекаться из базы данных.
Эта программа может стать хорошей отправной точкой, но из-за использования встроенных шрифтов GD в ней задействовано много «волшебных чисел», соответствующих высоте и ширине шрифта. Кроме того, интервалы между ответами тоже жестко фиксируются. Если вы перейдете на более современные шрифты (например, TrueType), алгоритмы управления этими числами придется обновить.
В начале функции определяется набор комбинаций RGB; они используются как цвета для рисования полос. Определяются различные константы — такие как $line_width, максимальное количество символов в строке. Переменная $bar_height определяет высоту полос, а $scale масштабирует длину полосы в зависимости от максимальной длины по всем полосам. Отступ $padding смещает результаты на пять пикселов от края холста.
Затем программа создает очень большой холст для рисования гистограммы; позднее холст обрезается до нужного размера, но определить максимальный размер заранее бывает достаточно сложно. В качестве фона гистограммы выбирается цвет #E0E0E0, то есть светло-серый. Чтобы ограничить ширину гистограммы до разумных пределов, функция wordwrap() разбивает текст вопроса $question до нужной длины строк, а затем вызывает для него explode() по завершителям \n. Так создается массив строк правильного размера, элементы которого перебираются в цикле для последовательного вывода строк.
После вывода вопроса можно переходить к ответам. Сначала числа в результатах форматируются функцией sprintf(). Для форматирования общего процента голосов для заданного ответа с одним знаком в дробной части используется спецификатор %1.1f.
Длина полосы, соответствующей результату, вычисляется аналогично, но вместо умножения на сто число умножается на масштабный множитель $scale, с получением целого результата. Цвет полосы извлекается из массива $colors, содержащего триплеты RGB. Затем функция ImageFilledRectangle() рисует полосу, а функция ImageString() выводит процент справа от полосы. После добавления отступов текст ответа выводится по тому же алгоритму, который использовался для вывода вопроса.
После вывода всех ответов общий размер гистограммы сохраняется в $y. Теперь изображение можно обрезать до нужного размера, но специальной функции обрезания не существует.
Чтобы обойти это ограничение, создайте новый холст нужного размера и вызовите ImageCopy() для той части исходного холста, которую нужно сохранить. Затем изображение правильного размера в формате PNG передается функцией ImagePNG(), а память освобождается двумя вызовами Image-Destroy().
Как было сказано в начале раздела, эта функция рисования гистограмм написана «на скорую руку». Она работает и успешно решает некоторые проблемы (например, обеспечивая перенос строк), но идеальной ее не назовешь. Например, возможности ее настройки сильно ограничены — большинство параметров встроены прямо в код. И все же она наглядно демонстрирует, как при помощи функций GD строится вполне практичное графическое приложение.

Предотвращение фиксации сеанса

Задача
Требуется принять меры к тому, чтобы идентификатор сеанса пользователя не мог быть предоставлен третьей стороной, например злоумышленником, который пытается перехватить сеанс пользователя.
Решение
Повторно генерируйте идентификатор сеанса функцией session_regenerate_id() при любых изменениях в привилегиях пользователя, например после успешного входа:

session_regenerate_id();
$_SESSION['logged_in'] = true;

В PHP 5.5.2 появился новый параметр конфигурации session.use_strict_mode, который помогает предотвратить перехват сеанса. Когда этот режим активен, PHP принимает только уже инициализированные идентификаторы сеансов. Если браузер отправляет новый идентификатор сеанса, PHP отвергает его и генерирует новый идентификатор.

Защита от фальсификации форм

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

<?php
session_start();
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
?>
<form action="buy.php" method="POST">
<input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>" />
<p>Stock Symbol: <input type="text" name="symbol" /></p>
<p>Quantity: <input type="text" name="quantity" /></p>
<p><input type="submit" value="Buy Stocks" /></p>
</form>

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

session_start();
if ((! isset($_SESSION['token'])) ||
    ($_POST['token'] != $_SESSION['token'])) {
    /* Запросить пароль у пользователя. */
} else {
    /* Продолжить. */
}
Обеспечение фильтрации входных данных

Задача
Требуется обеспечить фильтрацию всех входных данных перед использованием.
Решение
Инициализируйте пустой массив для хранения фильтрованных данных. Проверив некоторые данные на действительность, сохраните их в массиве:

$filters = array('name' => array('filter' => FILTER_VALIDATE_REGEXP,
                                 'options' => array('regexp' => '/^[a-z]+$/i')),
                 'age' => array('filter' => FILTER_VALIDATE_INT,
                                'options' => array('min_range' => 13)));
$clean = filter_input_array(INPUT_POST, $filters);

Жесткие правила назначения имен помогут следить за тем, какие входные данные были отфильтрованы. Инициализация $clean пустым массивом предотвращает возможность внедрения данных в массив; чтобы данные появились в нем, они должны быть добавлены явно. В приведенном коде вызов filter_input_array() инициализирует $clean так, чтобы массив мог содержать только фильтрованную информацию. Когда вы установите такие правила, как использование $clean, очень важно, чтобы в вашей бизнес-логике использовались только данные из этого массива.

Предотвращение межсайтовых сценарных атак

Задача
Требуется надежно защитить приложения PHP от межсайтовых сценарных атак (XSS, Cross-Site Scripting).
Решение
Экранируйте весь вывод HTML функцией htmlentities(), указав правильную кодировку символов:

/* Обратите внимание на кодировку. */
header('Content-Type: text/html; charset=UTF-8');
/* Инициализация массива для экранированных данных. */
$html = array();
/* Экранирование фильрованных данных. */
$html['username'] = htmlentities($clean['username'], ENT_QUOTES, 'UTF-8');
echo "<p>Welcome back, {$html['username']}.</p>";

Атаки XSS пытаются использовать ситуацию, при которой данные, предоставленные третьей стороной, включаются в HTML без должного экранирования. Хитроумный атакующий может предоставить код, который при интерпретации браузером будет чрезвычайно опасным для ваших пользователей. С htmlentities() вы можете быть уверены в том, что такие сторонние данные будут отображаться на экране, а не интерпретироваться браузером.

Предотвращение внедрения SQL

Задача
Требуется исключить уязвимости внедрения SQL в приложении PHP.
Решение
Воспользуйтесь библиотекой баз данных, выполняющей необходимое экранирование для базы данных, например PDO:

$statement = $db->prepare("INSERT
                             INTO   users (username, password)
                             VALUES (:username, :password)");
$statement->bindParam(':username', $clean['username']);
$statement->bindParam(':password', $clean['password']);
$statement->execute();

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

Сокрытие сообщений об ошибках от пользователей

Задача
Сообщения об ошибках PHP не должны быть видны пользователям.
Решение
Включите следующие значения в файл php .ini или конфигурационный файл веб-сервера:

display_errors =off
log_errors     =on

Если серверный файл php .ini недоступен, эти значения можно задать вызовами ini_set():

ini_set('display_errors', 'off');
ini_set('log_errors', 'on');

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

Выбор случайной строки из файла

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

$line_number = 0;
$fh = fopen(__DIR__ . '/sayings.txt','r') or die($php_errormsg);
while (! feof($fh)) {
    if ($s = fgets($fh)) {
        $line_number++;
        if (mt_rand(0, $line_number - 1) == 0) {
            $line = $s;
        }
    }
}
fclose($fh) or die($php_errormsg);

После чтения каждой строки счетчик $line_number увеличивается, а приведенный пример генерирует случайное целое число в диапазоне от 0 до $line_number - 1. Если число равно 0, то текущая строка сохраняется в переменной результата. После того как все строки будут прочитаны, последняя случайно выбранная строка остается в $line.
Этот элегантный алгоритм гарантирует, что каждая строка в файле может быть выбрана с вероятностью 1/n, не требуя при этом сохранения всех n строк в памяти.

Обработка всех файлов в каталоге

Задача
Требуется перебрать все файлы в каталоге. Например, вы хотите создать на форме поле ≪select/> со списком всех файлов в каталоге.
Решение
Воспользуйтесь классом DirectoryIterator для перебора всех файлов в каталоге:

echo "<select name='file'>\n";
foreach (new DirectoryIterator('/usr/local/images') as $file) {
    echo '<option>' . htmlentities($file) . "</option>\n";
}
echo '</select>';

Класс DirectoryIterator возвращает объект для каждого элемента каталога, включая . (текущий каталог) и .. (родительский каталог). К счастью, этот объект содержит методы, которые помогают определить тип представляемого элемента.
Метод isDot() возвращает true для элементов . или ... В следующем примере вызов isDot() предотвращает включение этих двух элементов в результат:

echo "<select name='file'>\n";
foreach (new DirectoryIterator('/usr/local/images') as $file) {
    if (! $file->isDot()) {
        echo '<option>' . htmlentities($file) . "</option>\n";
    }
}
echo '</select>';
Получение списка файлов по шаблону

Задача
Требуется найти все имена файлов, соответствующие заданному шаблону.
Решение
Используйте субкласс FilterIterator в сочетании с DirectoryIterator. Субкласс FilterIterator должен содержать метод accept(), который решает, подходит некоторое значение или нет.
Например, следующий код считает допустимыми только имена, заканчивающиеся стандартными расширениями графических файлов:

class ImageFilter extends FilterIterator {
    public function accept() {
        return preg_match('@\.(gif|jpe?g|png)$@i',$this->current());
    }
}
foreach (new ImageFilter(new DirectoryIterator('/usr/local/images')) as $img) {
    print "<img src='".htmlentities($img)."'/>\n";
}

Класс FilterIterator строится на основе DirectoryIterator и может отфильтровывать нежелательные элементы. Метод accept() решает, вернуть ли true или false для каждого элемента, для обращения к которому используется запись $this->current()). В Решении функция accept() использует регулярное выражение для принятия решения, но ваш код может использовать любую логику, которая вам понадобится.
Если правило может быть выражено простым шаблоном командного процессора (например, *.*), для получения подходящих имен файлов можно использовать функцию glob(). Например, следующий фрагмент находит все текстовые файлы в конкретном каталоге:

foreach (glob('/usr/local/docs/*.txt') as $file) {
   $contents = file_get_contents($file);
   print "$file contains $contents\n";
}

Функция glob() возвращает массив подходящих имен файлов. Если ни один файл не соответствует шаблону, функция glob() возвращает false.

Задача Требуется выполнить операцию со всеми файлами в каталоге и во всех его подкаталогах. Например, вы хотите узнать, какое дисковое пространство занимают все файлы в каталоге.
Решение
Воспользуйтесь классами RecursiveDirectoryIterator и Recursive Itera-torIterator. Класс RecursiveDirectoryIterator расширяет DirectoryIterator методом getChildren(), который предоставляет доступ к элементам подкаталога.
Класс RecursiveIteratorIterator преобразует иерархию, возвращаемую RecursiveDirectoryIterator, в один список. Следующий пример вычисляет общий размер файлов в каталоге:

$dir = new RecursiveDirectoryIterator('/usr/local');
$totalSize = 0;
foreach (new RecursiveIteratorIterator($dir) as $file) {
    $totalSize += $file->getSize();
}
print "The total size is $totalSize.\n";

Объекты, которые выдает RecursiveDirectoryIterator (а следовательно, и RecursiveIteratorIterator), не отличаются от объектов, получаемых от DirectoryIterator, поэтому для них доступны все методы.

Программа: вывод содержимого каталога веб-сервера

Программа web-ls .php (листинг 25.1) выводит список файлов в корневом каталоге документов веб-сервера, отформатированный по образцу вывода команды Unix ls. Имена файлов оформляются в виде ссылок, позволяющих загрузить каждый файл, а имена каталогов оформляются в виде ссылок для просмотра каждого каталога.
Заметная часть листинга 25.1 строит удобное представление разрешений файла, а основная часть программы находится в цикле foreach. Класс DirectoryIterator возвращает элемент для каждого элемента в каталоге.
Затем различные методы объекта элемента предоставляют информацию об этом файле, а функция printf() выводит отформатированную информацию об этом файле. Функция mode_string() и используемые ею константы преобразуют восьмеричное представление режима файла (например, 35316) в удобочитаемую строку (например, -rwsrw-r--).
Листинг 25 .1 . web-ls .php

/* Битовые маски для определения разрешений и типа файлов.
 * Приведенные ниже значения соответствуют стандарту POSIX;
 * некоторые системы могут использовать собственные расширения.
 */
define('S_IFMT',0170000);   // маска для всех типов
define('S_IFSOCK',0140000); // тип: сокет
define('S_IFLNK',0120000);  // тип: символическая ссылка
define('S_IFREG',0100000);  // тип: обычный файл
define('S_IFBLK',0060000);  // тип: блочное устройство
define('S_IFDIR',0040000);  // тип: каталог
define('S_IFCHR',0020000);  // тип: символьное устройство
define('S_IFIFO',0010000);  // тип: fifo
define('S_ISUID',0004000);  // бит set-uid
define('S_ISGID',0002000);  // бит set-gid
define('S_ISVTX',0001000);  // бит закрепления
define('S_IRWXU',00700);    // маска разрешений владельца
define('S_IRUSR',00400);    // владелец: разрешение чтения
define('S_IWUSR',00200);    // владелец: разрешение записи
define('S_IXUSR',00100);    // владелец: разрешение исполнения
define('S_IRWXG',00070);    // маска разрешений группы
define('S_IRGRP',00040);    // группа: разрешение чтения
define('S_IWGRP',00020);    // группа: разрешение записи
define('S_IXGRP',00010);    // группа: разрешение исполнения
define('S_IRWXO',00007);    // маска разрешений других пользователей
define('S_IROTH',00004);    // другие пользователи: разрешение чтения
define('S_IWOTH',00002);    // другие пользователи: разрешение записи
define('S_IXOTH',00001);    // другие пользователи: разрешение исполнения
/* mode_string() - вспомогательная функция, которая получает восьмеричное
 * значение режима и возвращает строку из 10 символов, представляющую
 * тип файла и разрешения. Это PHP-версия функции mode_string()
 * из пакета GNU fileutils.
 */
$mode_type_map = array(S_IFBLK => 'b', S_IFCHR => 'c',
                       S_IFDIR => 'd', S_IFREG => '-',
                       S_IFIFO => 'p', S_IFLNK => 'l',
                       S_IFSOCK => 's');
function mode_string($mode) {
    global $mode_type_map;
    $s = '';
    $mode_type = $mode & S_IFMT;
    // Добавление символа, обозначающего тип
    $s .= isset($mode_type_map[$mode_type]) ?
          $mode_type_map[$mode_type] : '?';
  // Назначение разрешений пользователя
  $s .= $mode & S_IRUSR ? 'r' : '-';
  $s .= $mode & S_IWUSR ? 'w' : '-';
  $s .= $mode & S_IXUSR ? 'x' : '-';
  // Назначение разрешений группы
  $s .= $mode & S_IRGRP ? 'r' : '-';
  $s .= $mode & S_IWGRP ? 'w' : '-';
  $s .= $mode & S_IXGRP ? 'x' : '-';
  // Назначение разрешений других пользователей
  $s .= $mode & S_IROTH ? 'r' : '-';
  $s .= $mode & S_IWOTH ? 'w' : '-';
  $s .= $mode & S_IXOTH ? 'x' : '-';
  // Изменение букв исполнения для битов set-uid, set-gid и закрепления
  if ($mode & S_ISUID) {
      // 'S' для set-uid без исполнения владельцем
      $s[3] = ($s[3] == 'x') ? 's' : 'S';
  }
  if ($mode & S_ISGID) {
      // 'S' для set-gid без исполнения группой
      $s[6] = ($s[6] == 'x') ? 's' : 'S';
  }
  if ($mode & S_ISVTX) {
      // 'T' для бита закрепления без исполнения другими пользователями
      $s[9] = ($s[9] == 'x') ? 't' : 'T';
  }
  return $s;
}
// Если каталог не задан, начать к корневого каталога документов
$dir = isset($_GET['dir']) ? $_GET['dir'] : '';
// Поиск $dir в файловой системе
$real_dir = realpath($_SERVER['DOCUMENT_ROOT'].$dir);
// Передача корневого каталога документов через realpath
// снимает все проблемы с обычной и обратной косой чертой
$real_docroot = realpath($_SERVER['DOCUMENT_ROOT']);
// Убедиться в том, что $real_dir находится
// внутри корневого каталога документов
if (! (($real_dir == $real_docroot) ||
       ((strlen($real_dir) > strlen($real_docroot)) &&
        (strncasecmp($real_dir,$real_docroot.DIRECTORY_SEPARATOR,
        strlen($real_docroot.DIRECTORY_SEPARATOR)) == 0)))) {
    die("$dir is not inside the document root");
}
// Привести $dir к канонической форме, отделив корневой каталог
// документа от начальной части
$dir = substr($real_dir,strlen($real_docroot)+1);
// Открывается каталог?
if (! is_dir($real_dir)) {
    die("$real_dir is not a directory");
}
print '<pre><table>';
// Прочитать каждый элемент в каталоге
foreach (new DirectoryIterator($real_dir) as $file) {
    // Преобразовать uid в имя пользователя
    if (function_exists('posix_getpwuid')) {
        $user_info = posix_getpwuid($file->getOwner());
    } else {
        $user_info = $file->getOwner();
    }
    // Преобразовать gid в имя группы
    if (function_exists('posix_getgrid')) {
        $group_info = $file->getGroup();
    } else {
        $group_info = $file->getGroup();
    }
    // Отформатировать дату для удобства чтения
    $date = date('M d H:i',$file->getMTime());
    // Преобразовать восьмеричное значение в понятную строку
    $mode = mode_string($file->getPerms());
    $mode_type = substr($mode,0,1);
    if (($mode_type == 'c') || ($mode_type == 'b')) {
        /* Для блочного или символьного устройства вывести
         * основной и дополнительный тип устройства
         * вместо размера файла */
        $statInfo = lstat($file->getPathname());
        $major = ($statInfo['rdev'] >> 8) & 0xff;
        $minor = $statInfo['rdev'] & 0xff;
        $size = sprintf('%3u, %3u',$major,$minor);
    } else {
        $size = $file->getSize();
    }
    // Отформатировать тег <a href=""> для имени файла.
    // Для текущего каталога ссылка не создается.
    if ('.' == $file->getFilename()) {
        $href = $file->getFilename();
    } else {
        // Не включать ".." в ссылку родительского каталога
        if ('..' == $file->getFilename()) {
            $href = urlencode(dirname($dir));
        } else {
            $href = urlencode($dir) . '/' . urlencode($file);
        }
        /* Закодированы должны быть все символы, кроме "/" */
        $href = str_replace('%2F','/',$href);
        // Другие каталоги должны просматриваться web-ls
        if ($file->isDir()) {
            $href = sprintf('<a href="%s?dir=/%s">%s</a>',
                            $_SERVER['PHP_SELF'],$href,$file);
        } else {
            // Если это файл, создать ссылку для загрузки
            $href= sprintf('<a href="%s">%s</a>',$href,$file);
        }
        // Если это символьная ссылка, показать целевой файл
        if ('l' == $mode_type) {
            $href .= ' -&gt; ' . readlink($file->getPathname());
        }
    }
    // Вывод информации о файле
    printf('<tr><td>%s</td><td align="right">%s</td>
            <td align="right">%s</td><td align="right">%s</td>
            <td align="right">%s</td><td>%s</td></tr>',
           $mode,                // Отформатированная строка режима
           $user_info['name'],   // Имя пользователя владельца
           $group_info['name'],  // Имя группы
           $size,                // Размер файла (или номера для устройств)
           $date,                // Дата и время последнего изменения
           $href);               // Ссылка для просмотра или загрузки
}
print '</table></pre>';
Программа: поиск по сайту на файлах

Программа site-search .php, может использоваться для выполнения поиска по малым и средним сайтам на базе файлов:

class SiteSearch {
    public $bodyRegex = '';
    protected $seen = array();
    public function searchDir($dir) {
        // Массив для хранения подходящих страниц
        $pages = array();
        // Массив для каталогов, которые будут использоваться
        // при рекурсивном просмотре
        $dirs = array();
        // Каталог помечается как просмотренный,
        // чтобы не возвращаться к нему в будущем
        $this->seen[realpath($dir)] = true;
        try {
            foreach (new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir)) as $file) {
                if ($file->isFile() && $file->isReadable() &&
                (! isset($this->seen[$file->getPathname()]))) {
                    // Пометить путь как просмотренный,
                    // чтобы пропустить его, если
                    // мы вернемся к нему в будущем
                    $this->seen[$file->getPathname()] = true;
                    // Загрузить содержимое файла в $text
                    $text = file_get_contents($file->getPathname());
                    // Если условие поиска находится в теле страницы
                    if (preg_match($this->bodyRegex,$text)) {
                    // Построить относительный URI файла,
                    // исключив корневой каталог документов из полного пути
                    $uri = substr_replace($file->getPathname(),'',0,strlen
                    ($_SERVER['DOCUMENT_ROOT']));
                    // Если у страницы есть заголовок, получить его
                    if (preg_match('#<title>(.*?)</title>#Sis',$text,$match)) {
                        // Добавить заголовок и URI в $pages
                        array_push($pages,array($uri,$match[1]));
                    } else {
                        // В противном случае использовать URI как заголовок
                        array_push($pages,array($uri,$uri));
                    }
                }
                }
            }
        } catch (Exception $e) {
            // Ошибка при открытии каталога
        }
        return $pages;
    }
}
// Вспомогательная функция для алфавитной сортировки
// страниц по заголовкам
function by_title($a,$b) {
        return ($a[1] == $b[1]) ?
               strcmp($a[0],$b[0]) :
               ($a[1] > $b[1]);
}
// Объект SiteSearch для выполнения поиска
$search = new SiteSearch();
// Массив для страниц, подходящих по условию поиска
$matching_pages = array();
// Подкаталоги корневого каталога документов, в которых
// должен выполняться поиск
$search_dirs = array('sports','movies','food');
// Регулярное выражение для поиска. Модификатор "S" приказывает
// ядру PCRE проанализировать регулярное выражение
// для повышения эффективности.
$search->bodyRegex = '#<body>(.*' . preg_quote($_GET['term'],'#').
                     '.*)</body>#Sis';
// Добавить подходящие файлы в каждом каталоге в $matching_pages
foreach ($search_dirs as $dir) {
    $matching_pages = array_merge($matching_pages,
                      $search->searchDir($_SERVER['DOCUMENT_ROOT'].'/'.$dir));
}
if (count($matching_pages)) {
    // Отсортировать подходящие страницы по заголовкам
    usort($matching_pages,'by_title');
    print '<ul>';
    // Вывести каждый заголовок со ссылкой на страницу
    foreach ($matching_pages as $k => $v) {
        print sprintf('<li> <a href="%s">%s</a>',$v[0],$v[1]);
    }
    print '</ul>';
} else {
    print 'No pages found.';
}

Программа ищет заданное условие (из $_GET['term']) во всех файлах заданного набора каталогов, находящихся в корневом каталоге документов. Список этих каталогов хранится в $search_dirs. Кроме того, программа перебирает подката- логи и переходит по символическим ссылкам, но при этом отслеживает посещен- ные файлы и каталоги, чтобы избежать зацикливания. Если будут найдены какие-либо страницы, содержащие искомое условие, про- грамма выводит список ссылок на эти страницы, упорядоченный в алфавитном порядке заголовков. Если страница не имеет заголовка (между тегами <title> и </title>), то используется относительный URI страницы.
Программа ищет искомое условие между тегами <body> и </body> в каждом файле. Если в тегах <body> содержится большое количество текста, который нужно исключить из поиска, заключите текст, в котором должен проводиться поиск, в специальные комментарии HTML, а затем измените $body_regex, чтобы поиск ограничивался этими тегами. Допустим, страница выглядит так:

<html>
<head>
        <title>Your Title</title>
</head>
<body>
// Разметка HTML для меню  и т. д.
<!-- search-start -->
<h1>Aliens Invade Earth</h1>
<h3>by H.G. Wells</h3>
<p>Aliens invaded earth today. Uh Oh.</p>
// ...
<!-- search-end -->
// Разметка HTML для завершителей  и т. д.
</body>
</html>

Чтобы поиск ограничивался названием книги, автором и кратким описанием в комментариях HTML, замените $search->bodyRegex следующим значением:

$search->bodyRegex = '#<!-- search-start -->(.*' . preg_quote($_GET['term'],'#').
              '.*)<!-- search-end -->#Sis';

Если вы не хотите, чтобы условие поиска совпадало с текстом, находящимся в тегах HTML или PHP, добавьте вызов strip_tags() в код, загружающий содержимое файла для поиска:

// Загрузить содержимое файла в $text
$text = strip_tags(file_get_contents($file->getPathname()));

PHP. Рецепты программированияPHP. Рецепты программирования 785 страниц · 2016 · 5.76 MB · русский by Скляр Д. & Трахтенберг А.
 
на главную сниппетов
Курсы