Архив ‘PHP’

Объединение JavaScript и CSS-файлов для ускорения загрузки

Четверг, 28 февраля, 2008

Когда страница использует большое количество внешних JavaScript-файлов или скриптов CSS (а такое часто бывает, если используются библиотеки типа Prototype или jQuery), время ее загрузки существенно растет, так как каждый такой файл — это новое обращение к серверу, т.е. трата нескольких сотен миллисекунд. Но тем не менее, есть способ избежать эту проблему: воспользоваться серверным скриптом, который объединит несколько указанных файлов в один и отдаст его броузеру. Таким образом, выполняется всего один запрос к серверу для JavaScript и один запрос для CSS.

Существует уже готовое решение. Для того, чтобы воспользоваться им, нужно скачать и поместить в корень сайта файл combine.phps, а затем прописать в .htaccess такие правила для mod_rewrite:

RewriteEngine On
RewriteBase /
RewriteRule ^css/(.*\.css) /combine.php?type=css&files=$1
RewriteRule ^javascript/(.*\.js) /combine.php?type=javascript&files=$1

Далее можно подключать JavaScript-файлы, просто перечисляя их через запятую с помощью таких конструкций:

<script src="/javascript/my1.js,my2.js,jquery.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css,my1.css,my2.css"> 

Если файлы физически расположены в каком-то другом каталоге, то достаточно изменить переменные в самом файле combine.phps:

	$cachedir = dirname(__FILE__) . '/cache';
	$cssdir   = dirname(__FILE__) . '/css';
	$jsdir    = dirname(__FILE__) . '/javascript';

на нужные пути.

К недостаткам данного способа можно отнести обязательное наличие mod_rewrite на сервере. Впрочем, в большинстве броузеров будет работать и прямой вызов скрипта (потрбуется переименовать его из combine.phps в combine.php) вида “/combine.php?type=javascript&files=my1.js,my2.js,jquery.js".

Загрузка файла с возможностью докачки

Среда, 6 февраля, 2008

В HTTP-протоколе предусмотрена возможность получения части загружаемого файла или страницы. Для в запросе передается специальный параметр Range, который имеет вид Range: bytes=начало-конец. В ответе вместо обычного статуса 200 Ok выдается статус 206 Partial Content и добавляются поля Accept-Ranges: bytes и Content-Range: bytes начало-конец/общая_длина, при этом в поле Content-Length указывается длина только той части, которая передается клиенту.

На PHP это можно реализовать примерно так:

function send_file($filename) {
// примечание: в $filename -- полное имя файла, который нужно отдать клиенту
$length=filesize($filename);
if ($_SERVER["HTTP_RANGE"]) { // проверяем, пришел ли заголовок Range
  $range = $_SERVER["HTTP_RANGE"];
  $range = str_replace('bytes=','', $range);
  list($range_start,$range_end) =explode("-", $range);
  header('HTTP/1.1 206 Partial Content');
  header('Accept-Ranges: bytes');
  header('Content-Range: bytes '.intval($range_start).'-'.intval($range_end).'/'.$length);
}
else {
  $range_start=0;
  $range_end=$length-1;
  header('HTTP/1.1 200 Ok');
}
header('Content-Length: '.($range_end-$range_start+1));
header('Content-Type: application/octet-stream');
header('content-disposition: attachment; filename="'.basename($filename).'"');
header("Last-Modified: ".date('r',filemtime($filename)));
$fh=fopen($filename,'rb');
fseek($fh,$range_start);
$position=$range_start;
$bufsize=1*1024*1024;
while ($buffer=fread($fh,$bufsize) && $position<$range_end) {
  if ($position+$bufsize>$range_end) $buffer=substr($buffer,0,$range_end-$position);
  $position+=$bufsize;
  print $buffer;
}
fclose($fh);

Следует отметить, что поле content-disposition должно писаться со строчной (малой) буквы, т.к. в противном случае его некорректно воспринимает броузер Opera.

GZIP-сжатие страниц, сгенерированных PHP-скриптом

Вторник, 5 февраля, 2008

Для того, чтобы включить сжатие результатов выдачи PHP-скрипта, достаточно вызвать функцию ob_start(’ob_gzhandler’). Однако в случае, если в настройках PHP включена настройка zlib.output_compression, происходит конфликт, в результате которого генерируется предупреждение. Чтобы этого не происходило, можно воспользоваться таким кодом:

if (!ini_get('zlib.output_compression')) {
  ob_start('ob_gzhandler',9); // 9 -- уровень сжатия
}
else { ob_start(); }

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

Проверка имени файла на безопасность

Понедельник, 28 января, 2008

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

Таковыми в частности являются:

/ или \ в начале имени может позволить обратиться к произвольному месту в файловой системе (если не указан текущий путь, это позволит открывать файлы в любом месте файловой системы, а не только ниже текущего каталога);

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

~ (тильда) — если интерпретатор выполняется от реального пользователя, а не от nobody (т.е. PHP запускается как CGI), позволяет открыть файл в его родительском каталоге;

` (обратный апостроф) — в некоторых ситуациях может позволить выполнить внешнюю команду на сервере;

://(часть, отделяющая протокол в URL от доменного имени) — при разрешенном подключении внешних файлов позволит загрузить и выполнить произвольный PHP-код.

Кроме того, ряд символов может привести к созданию нечитаемых/неудаляемых файлов, вызывать ошибку при записи или ошибку в HTML при формировании ссылки, поэтому их тоже желательно фильтровать. К таким символам относятся: двоеточие (:), точка с запятой (;), запятая (,), кавычки (”), апостроф (’), амперсанд (&), символы больше и меньше (<>).

В результате получаем следющую процедуру проверки:

function check_name( $filename) {
    $result=(substr($filename,0,1)!='/' && substr($filename,0,1)!='\\');
    if ($result) {
      $test=array('..','://','~','`','\'','"',':',';',',','&','>','<');
      for ($i=0, $count=count($test); $i<$count && $result; $i++) $result=(strpos($filename,$test[$i])===false);
    }
    return $result;
}

Кроме того, при загрузке файла имеет смысл сохранять его с таким именем, чтобы пользователь не мог его угадать (лучше всего для этой цели подходит MD5-хеш от имени + размера + времени загрузки + случайного значения), и не давать возможности узнать имя файла из URL ссылки для его скачивания/просмотра (т.е. если данные о загруженном файле хранятся в таблице БД, то скрипту для скачивания передавать номер этого файла в таблице, а не его имя).

О корректном использовании flock

Среда, 23 января, 2008

Как известно, flock используется для блокировки файлов с целью предотвращения одновременного доступа к ним двух скриптов. Но следует учитывать, что простой вызов flock($fh,$mode) не на всех платформах приводит к ожиданию блокировки файла в том случае, если ее не удалось выполнить сразу. Поэтому более правильно вызывать эту функцию следующим образом:

$fh=fopen('filename','r');
if (!$fh) {echo 'Error!'; }
else {while (!flock($fh,$mode)) sleep(1);
// обработка файла
fclose($fh);
}

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

Простой перенос данных из форм в несколько таблиц

Понедельник, 7 января, 2008

Часто бывают ситуации, когда требуется данные, которые пользователь вводит в форму, разместить в нескольких разных таблицах базы данных. Наиболее простым и красивым решением в таком случае будет именовать поля в форме следующим образом: имя_таблицы[имя_столбца]. В этом случае PHP при получении данных создаст в массиве $_POST (или $_GET) подмассивы, в каждом из которых будут только те элементы, которые относятся к одной и той же таблице. Остается только написать функцию, которая будет сохранять все поля массива в таблицу.

Пример такой ситуации: форма содержит поля topic['title'], topic['descr'], post['text'], post['date'], download[url], download[homepage].

В этом случае обрабатывать ее можно будет примерно таким образом:

$topic_id=$db->save('topic',$_POST['topic']);
$post_id=$db->save('post',$_POST['post']);
$_POST['download']['id']=$topic_id; // таблица download привязывается к таблице Topic по id
$db->save('download',$_POST['download']);

Совет: при необходимости можно организовать цикл по массиву таблиц вместо того, чтобы вызывать функцию для каждой таблицы поштучно. Но при этом список таблиц должен все равно указываться явно (т.е. нельзя проходить циклом по $_POST и пытаться сохранить все, что является подмассивом, хотя в первый момент такое решение кажется привлекательным), так как в противном случае пользователь может изменить форму и внести данные в те таблицы, в которые делать этого не предполагалось (например, в таблицу прав доступа).

О защите от XSS-атак

Воскресенье, 6 января, 2008

При разработке систем, в которых у пользователя есть возможность авторизации и отправки сообщений, существует ряд уязвимостей, с помощью которых можно организовать XSS-атаку путем внедрения JavaScript для отсылки cookies (или URL с идентификатором сессии).

Типичными способами являются следующие:

1. Самый очевидный и наиболее просто блокируемый: вставка тега <script> (отслеживается регулярным выражением вида

preg_replace('|<script(.*?)</script(.*?)>|is','',$buffer)

2. Добавление тега с обработчиком JavaScript-события (конструкции вида <div onMouseOver=”alert(’test’)”></div>. Такие регулярные выражения можно отследить следующим куском кода:

$events = array('onLoad','onClick'); // массив событий
$count=preg_ match_all('|<\w+(\s+[^>]*)>|s',$buffer,$matches);
for ($i=0; $i<$count; $i++) {
  $oldbuf=$matches[0][$i];
  foreach ($events as $curevent) {
    preg_replace('|\s+'.$curevent.'=".*?"|is','',$matches[1][$i]); // вместо '' можно вставить какое-нибудь уведомление о замене
    preg_replace('|\s+'.$curevent.'=\'.*?\'|is','',$matches[1][$i]);
    preg_replace('|\s+'.$curevent.'=\S*|is','',$matches[1][$i]);
  }
  $buffer=str_replace($oldbuf,$matches[0][$i],buffer); // замена исходной строки на очищенную от некорретных обработчиков
}

3. Добавление JavaScript в адрес ссылки или рисунка: (<a href=”javascript:alert(’test’)”></a>).
Корректируется следующим регулярными выражениями:

preg_replace('|<a href="\w+script:.*?"[^>]+>.*?</a>|is','',$buffer);
preg_replace('|<a href=\'\w+script:.*?\'[^>]+>.*?</a>|is','',$buffer);
preg_replace('|<a href=\w+script:\S*[^>]+>.*?</a>|is','',$buffer);

4. Добавление JavaScript в url внутри параметра style, например: <div sytle=”background: url(’javascript:alert(\’test\’)')”></div>

Корректируется следующим куском кода:

$count=preg_ match_all('|<\w+(\s+[^>]*)>|s',$buffer,$matches);
for ($i=0; $i<$count; $i++) {
  $oldbuf=$matches[0][$i];
  preg_replace('|\s+style=".*?"|is','',$matches[1][$i]); // вместо '' можно вставить какое-нибудь уведомление о замене
  preg_replace('|\s+style=\'.*?\'|is','',$matches[1][$i]);
  preg_replace('|\s+style=\S*|is','',$matches[1][$i]);
  $buffer=str_replace($oldbuf,$matches[0][$i],buffer); // замена исходной строки на очищенную от некорретных обработчиков
}

Но кроме этих, довольно широко известных способов реализации XSS, имеется еще ряд малоизвестных особенностей, которые следует учитывать:

1. Броузер MSIE позволяет писать название протокола не только слитно, но и через символы табуляции (а также, возможно, перевода строки), т.е. ссылка вида <a href=”javas cript:alert(’test’)”>Тест</a> в нем будет работоспособна. Поэтому имеет смысл сначала проводить удаление символов табуляции внутри ссылок

2. В броузере MSIE, кроме JavaScript, существует еще и Visual Basic Script, который также может быть внедрен в URL ссылок и рисунков. Он обозначается vbscript (код в примере 3 написан с учетом его возможного наличия).

3. Если пользователю разрешается добавлять прикрепленные файлы и они сохраняются на диск в доступном для просмотра с помощью броузера месте с расширениями .htm, .html , то возможна атака следующего типа: в HTML-файл помещается код JavaScript, и затем атакуемому с помощью средств социальной инженерии предлагается перейти по ссылке, ведущей на этот файл напрямую, вместо того, чтобы скачать его скриптом.

Кроме того, даже если существует проверка на то, что загружаемый файл является графическим, внедрение JavaScript-кода в файл все равно возможно через так называемые EXIF-параметры (например. вместо имени автора можно прописать <script>alert(’test’)</script>), если файл сохраняется на диск с расширениями .html или .htm.

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

Проверка загрузки расширения (extension) в PHP

Вторник, 25 декабря, 2007

Иногда в PHP требуется проверить, загружено ли то или иное расширение, чтобы избежать фатальной ошибки, выдать пользователю дружественное сообщение и корректно завершить скрипт. Делается это с помощью функции extension_loaded, параметром которой является строка с именем расширения (с учетом регистра), а результатом — двоичное значение.

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

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

Пример кода, который выполняет проверку и пытается подгрузить расширение:

if (!extension_loaded('gd')) {
  $prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : '';
  if (!dl($prefix . 'sqlite.' . PHP_SHLIB_SUFFIX)) {
    echo 'Ошибка загрузки расширения GD!';
    return;
  }
}

// вызов функций GD

Как избежать файлов, созданных nobody

Воскресенье, 16 декабря, 2007

Если PHP выполняется как модуль сервера Apache, то при загрузке файлов на сервер через PHP-скрипт возникает весьма неприятная ситуация: владельцем файла оказывается пользователь, от имени которого выполняется скрипт (обычно какой-нибудь nobody или www), в результате чего файл становится недоступным (или доступным только на чтение) пользователю, заходящему через FTP. Можно попытаться решить эту проблему через команды chmod/chown, но многие хостеры запрещают вызов этих функций из PHP, поэтому такой вариант будет работать не всегда.

Но есть другой способ обойти эту проблему. Реализуется он следующим образом: загружаемый файл сохраняется в какой-то временный каталог, а далее скрипт устанавливает соединение с localhost по FTP с данными пользователя-владельца аккаунта и загружает этот файл уже туда, где он будет храниться постоянно. В результате владельцем аккаунта оказывается пользователь, от имени которого осуществляется доступ по FTP и проблем с недоступностью файла не возникает. Точно такой же способ можно применять, когда на этапе инсталляции требуется создать несколько подкаталогов и выставить на них определенные права.

Код, реализующий эту идею, выглядит примерно так (здесь реализовано 3 режима: простая загрузка файла — 0, загрузка файла с chmod — 1 и загрузка файла по FTP — 2, режим определяется значением константы CONFIG_upload_mode):

   function put_file($tmpname,$newname) {
    if (!defined('CONFIG_upload_mode')) define('CONFIG_upload_mode',0);
    if (!defined('CONFIG_upload_root')) define('CONFIG_upload_root','.');
    $result=false;
    if (CONFIG_upload_mode==2 && defined('CONFIG_upload_user') && defined('CONFIG_upload_pass')) {
      $ftp=ftp_connect('localhost');
      $result=ftp_login($ftp,CONFIG_upload_user,CONFIG_upload_pass);
      $result=$result && ftp_chdir($ftp,CONFIG_upload_root);
      $result=$result && ftp_put($ftp,$newname,$tmpname);
      $result=$result && ftp_chmod($ftp,0666,$newname);
      ftp_close($ftp);
    }
    elseif (CONFIG_upload_mode==1) {
      $result=copy($tmpname,$newname);
      chmod(0666,$newname);
    }
    else $result=copy($tmpname,$newname);
    if ($result) unlink($tmpname);
    return $result;
  }

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


Rambler's Top100