Фильтрация и проверка данных PHP. Частые ошибки

Материал предназначен в основном для начинающих веб-программистов.

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

Здесь я постараюсь описать как можно подробнее частые ошибки при фильтрации данных в PHP скрипте и дать простые советы как правильно выполнить фильтрацию данных.

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

Разбор полетов.

Фильтрация. Ошибка №1

Для числовых переменных используется такая проверка:

$number = $_GET['input_number'];
if (intval($number))
{
... выполняем SQL запрос ...
}


Почему она приведет к SQL инъекции? Дело в том, что пользователь может указать в переменной input_number значение:
1'+UNION+SELECT


В таком случаи проверка будет успешно пройдена, т.к. функция intval получает целочисленное значение переменной, т.е. 1, но в самой переменной $number ничего не изменилось, поэтому весь вредоносный код будет передан в SQL запрос.
Правильная фильтрация:
$number = intval($_GET['input_number']);
if ($number)
{
... выполняем SQL запрос ...
}


Конечно, условие может меняться, например если вам нужно получить только определенный диапазон:
if ($number >= 32 AND $number <= 65)



Если вы используете чекбоксы или мультиселекты с числовыми значениями, выполните такую проверку:
$checkbox_arr = array_map('intval', $_POST['checkbox']);


array_map
Так же встречаю фильтрацию в виде:
$number = htmlspecialchars(intval($_GET['input_number']));

htmlspecialchars
Или:
$number = mysql_escape_string(intval($_GET['input_number']));


mysql_escape_string

Ничего кроме улыбки это не может вызвать :)

Фильтрация. Ошибка №2.

Для стринг-переменных используется такая фильтрация:
$input_text = addslashes($_GET['input_text']);


Функция addslashes экранирует спец. символы, но она не учитывает кодировку БД и возможен обход фильтрации. Не стану копировать текст автора, который описал данную уязвимость и дам просто ссылку Chris Shiflett (перевод можно поискать в рунете).

Используйте функцию mysql_escape_string или mysql_real_escape_string, пример:
$input_text = mysql_escape_string($_GET['input_text']);


Если вы не предполагаете вхождение html тегов, то лучше всего сделать такую фильтрацию:
$input_text = strip_tags($_GET['input_text']);
$input_text = htmlspecialchars($input_text);
$input_text = mysql_escape_string($input_text);

strip_tags — убирает html теги.
htmlspecialchars — преобразует спец. символы в html сущности.
Так вы защитите себя от XSS атаки, помимо SQL инъекции.
Если же вам нужны html теги, но только как для вывода исходного кода, то достаточно использовать:
$input_text = htmlspecialchars($_GET['input_text']);
$input_text = mysql_escape_string($input_text);



Если вам важно, чтобы значение переменной не было пустой, то используйте функцию trim, пример:
$input_text = trim($_GET['input_text']);
$input_text = htmlspecialchars($input_text);
$input_text = mysql_escape_string($input_text);



Фильтрация. Ошибка №3.

Она касается поиска в БД.
Для поиска по числам используйте фильтрацию, описанную в первой ошибке.
Для поиска по тексту используйте фильтрацию, описанную во второй ошибке, но с оговорками.
Для того, чтобы пользователь не смог выполнить логическую ошибку, нужно удалять или экранировать спец. символы SQL.
Пример без доп. обработки строки:
$input_text = htmlspecialchars($_GET['input_text']); // Поиск: "%"
$input_text = mysql_escape_string($input_text);


На выходе у нас получится запрос вида:
... WHERE text_row LIKE '%".$input_text."%' ... // WHERE text_row LIKE '%%%'


Это значительно увеличит нагрузку на базу.
В своём скрипте я использую функцию, которая удаляет нежелательные мне символы из поиска:
function strip_data($text)
{
    $quotes = array ("\x27", "\x22", "\x60", "\t", "\n", "\r", "*", "%", "<", ">", "?", "!" );
    $goodquotes = array ("-", "+", "#" );
    $repquotes = array ("\-", "\+", "\#" );
    $text = trim( strip_tags( $text ) );
    $text = str_replace( $quotes, '', $text );
    $text = str_replace( $goodquotes, $repquotes, $text );
    $text = ereg_replace(" +", " ", $text);
            
    return $text;
}


Конечно, не все из выше перечисленных символов представляют опасность, но в моём случаи они не нужны, поэтому выполняю поиск и замену.
Пример использования фильтрации:
$input_text = strip_data($_GET['input_text']);
$input_text = htmlspecialchars($input_text);
$input_text = mysql_escape_string($input_text);


Также советую сделать ограничение по количеству символов в поиске, хотя бы не меньше 3-х, т.к. если у вас будет большое количество записей в базе, то поиск по 1-2 символам будет значительно увеличивать нагрузку на БД.

Фильтрация. Ошибка №4.

Не фильтруются значения в переменной $_COOKIE. Некоторые думаю, что раз эту переменную нельзя передать через форму, то это гарантия безопасности.
Данную переменную очень легко подделать любым браузером, отредактировав куки сайта.
Например, в одной известной CMS была проверка, используемого шаблона сайта:
if (@is_dir ( MAIN_DIR . '/template/' . $_COOKIE['skin'] )){
	$config['skin'] = $_COOKIE['skin'];
}
$tpl->dir = MAIN_DIR . '/template/' . $config['skin'];


В данном случаи можно подменить значение переменной $_COOKIE['skin'] и вызвать ошибку, в результате которой вы увидите абсолютный путь до папки сайта.
Если вы используете значение куков для сохранения в базу, то используйте одну из выше описанных фильтраций, тоже касается и переменной $_SERVER.

Фильтрация. Ошибка №5.

Включена директива register_globals. Обязательно выключите её, если она включена.
В некоторых ситуациях можно передать значение переменной, которая не должна была передаваться, например, если на сайте есть группы, то группе 2 переменная $group должна быть пустой или равняться 0, но достаточно подделать форму, добавив код:
<input type="text" name="group" value="5" />


В PHP скрипте переменная $group будет равна 5, если в скрипте она не была объявлена со значением по умолчанию.

Фильтрация. Ошибка №6.

Проверяйте загружаемые файлы.
Выполняйте проверку по следующим пунктам:
1. Расширение файла. Желательно запретить загрузку файлов с расширениями: php, php3, php4, php5 и т.п.
2. Загружен ли файл на сервер move_uploaded_file
3. Размер файла

Проверка. Ошибка №1.

Сталкивался со случаями, когда для AJAX запроса (например: повышение репутации) передавалось имя пользователя или его ID (кому повышается репутация), но в самом PHP не было проверки на существование такого пользователя.
Например:
$user_id = intval($_REQUEST['user_id']);
... INSERT INTO REPLOG SET uid = '{$user_id}', plus = '1' ...
... UPDATE Users SET reputation = reputation+1 WHERE user_id = '{$user_id}' ...


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

Проверка. Ошибка №2.

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

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

Проверка. Ошибка №3.

При использовании нескольких php файлов сделайте простую проверку.
В файле index.php (или в любом другом главном файле) напишите такую строчку перед подключением других php файлов:
define ( 'READFILE', true );


В начале других php файлов напишите:
if (! defined ( 'READFILE' ))
{
	exit ( "Error, wrong way to file.<br><a href=\"/\">Go to main</a>." );
}


Так вы ограничите доступ к файлам.

Проверка. Ошибка №4.

Используйте хеши для пользователей. Это поможет предотвратить вызов той или иной функции путём XSS.
Пример составления хеша для пользователей:
$secret_key = md5( strtolower( "http://site.ru/" . $member['name'] . sha1($password) . date( "Ymd" ) ) ); // $secret_key - это наш хеш


Далее во все важные формы подставляйте инпут со значением текущего хеша пользователя:
<input type="hidden" name="secret_key" value="$secret_key" />


Во время выполнения скрипта осуществляйте проверку:
if ($_POST['secret_key'] !== $secret_key)
{
exit ('Error: secret_key!');
}



Проверка. Ошибка №5.

При выводе SQL ошибок сделайте простое ограничение к доступу информации. Например задайте пароль для GET переменной:
if ($_GET['passsql'] == "password")
{
... вывод SQL ошибки ...
}
else
{
... Просто информация об ошибке, без подробностей ...
}


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

Проверка. Ошибка №5.
Старайтесь не подключать файлы, получая имена файлов извне.
Например:
if (isset($_GET['file_name']))
{
include $_GET['file_name'] .'.php';
}


Используйте переключатель switch:
switch($_GET['file_name'])
{         
         case 'file_1':
         include 'file_1.php';    
         break;     
         
         default:
         include 'file_0.php';    
         break;
}


В таком случаи вы предотвратите подключение файлов, которые не были вами предусмотрены.

Совет.

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

UPD: Поправил пост. Перенес все советы по поводу функций и переменных, которые были в комментариях.

Добавлено: 19 Ноября 2021 07:27:05 Добавил: Андрей Ковальчук

Простые правила работы с UTF-8 кодировкой в PHP

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

Я думаю, не надо рассказывать, что UTF-8 уже является стандартом для большинства web-приложений и пока не планируется особых замен. Но на текущий момент множество начинающих "познавателей" PHP сталкиваются с проблемами в этом месте и постят кучу топиков в связи с тем, что PHP пока не поддерживает кодировку UTF-8 "из коробки" и приносит вышеуказанным людям кучу непонимания и проблем. Кракозяблики, кракозяблики, кракозяблики... Проблема с UTF-8... Что делать? Помогите - слышно тут и там... На самом деле всё очень просто. Ниже я опишу несколько правил, которые позволяют избавиться от "проблем" с UTF-8 при разработке приложений на PHP.

Для начала, когда, пользуясь здравым смыслом, мы решаем работать с UTF-8 для простоты дела мы утверждаем, что от ныне абсолютно всё у нас будет храниться, писаться, выводиться в UTF-8. Второе, что нам будет нужно, это наличие php_mbstring расширения на сервере, которое предоставляет функции для работы со строками в многобайтном режиме. Для тех, кто в танке это функции, которые начинаются на mb_. Если ваш хостинг провайдер не потрудился установить у себя это расширение - смело выбирайте другого. Итак, если если ваш php поддерживает mb_ функции - пол дела сделано. Остальные пол дела для работы с UTF-8 в PHP заключаются в следующем:

1. В начале работы скрипта необходимо указать кодировку входа и выхода с помощью функции mb_internal_encoding:

Listing №1 (PHP)

mb_internal_encoding('UTF-8');

Также это позволит определить кодировку по умолчанию для всех mb-функций, которым нужно её указывать.

2. При работе со строками, преобразованиями символов, необходимо использовать функции работы со строками из расширения php_mbstring. Это, например, такие как mb_substr, mb_strpos, mb_strlen и т.п. или же те функции, которые безопасны для обработки данных в двоичной форме, такие как explode, str_replace и т.д. Эта особенность указана в мануале по этим функциям. Иногда нельзя просто заменить функцию на её mb-аналог, потому как такой там просто нет. Например, ucfirst, которая преобразует первый символ строки в верхний регистр. Решение тут очень простое - изящной комбинацией mb_substr и mb_strtoupper получаем необходимый результат. Попробуйте проделать это сами )). Также в этот пункт стоит отнести такие функции как htmlspecialchars, htmlentities и т.п., которые производят различные преобразования строк. Пожалуйста, передавайте в них третий аргумент $charset в виде строки 'UTF-8'.

3. Так как мы используем строки символов в самих скриптах, то строго необходимо сохранять их в UTF-8 кодировке. Это также относиться ко всем остальным файлам, которые вы используете - HTML шаблоны, различные подключаемые файлы, текстовые и т.д. Это всё должен делать ваш редактор кода. Согласуйте с ним эти вещи. Но смотрите внимательно, что бы он кодировал или преобразовывал файлы в UTF-8 без особой метки, называемой BOM. Так будет намного лучше.

4. Немаловажным фактором, является работа браузера с кодировкой UTF-8, а точнее с данными, которые вы посылаете ему, и теми данными, которые он отсылает вам. Для того, что бы браузер правильно понимал в какой кодировке вы отправили ему данные нужно отсылать заголовок с указанием кодировки. Это можно сделать так:

Listing №2 (PHP)
header('Content-Type: text/html; charset=UTF-8');

Только не забудьте отправлять этот заголовок до какого либо вывода с помощью echo, print или просто пробелов в начале скрипта до тега <?php или где то между парой этих тегов ?> <?php. Ну, а для того, чтобы браузер отсылал вам данные в нужной кодировке, нужно помечать сам html документ соответствующим мета-тегом. Например таким:

Listing №3 (HTML)
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

5. Наверное больше всего трудностей вызывает работа с базой данных. Обычно используется MySQL СУРБД. Тут всё тоже очень просто. Перед тем как послать и принимать данные нужно сказать серверу базы данных, что вы будете отсылать ей данные в кодировке UTF-8. Не вдаваясь в особые подробности, скажу лишь, что перед любым обращением к базе лишь единожды, т.е. один раз, нужно выполнить два запроса. Вот они:

Listing №4 (SQL)
SET NAMES "utf8"
-- и
SET CHARACTER SET "utf8"

Поверьте, она с радостью воспримет ваш призыв. Но не только обмениваться, а и хранить данные нужно соответственно. Посему изначально создавайте поля, хранящие строки в UTF-8 кодировке. Я думаю, вам не составит труда указать это при создании таблицы ;)


Ну вот и всё. Придерживаясь этих правил, и, я думаю, вы больше не будете встречать каких-то проблем с UTF-8 в PHP, по крайней мере до выхода шестой его версии. Хотя я знаю некоторые ситуации, но эта заметка не про них.

Добавлено: 08 Августа 2018 07:58:54 Добавил: Андрей Ковальчук

Разделы и абзацы

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

Но, в отличие от большинства процессоров, HTML и XHTML явным образом применяют теги раздела <div>, абзаца <p> и конца строки
для управления выравниванием и потоком текста. Возврат каретки1, хотяи очень полезный для удобства чтения, обычно игнорируется броузером, и авторы должны применять тег
, чтобы явно указать конец
строки. Тег <p>, который также вызывает переход на следующую строку, несет в себе дополнительный смысл помимо возврата каретки.

Тег <div> несколько отличается от тегов <p> и
. Включенный впервые в стандарт HTML 3.2, он был задуман, чтобы служить простым средством организации текста и разбивать документ на отдельные куски. В силу смысловой неопределенности этого тега он оставался непопулярным. Но последние нововведения (атрибуты выравнивания и стилей, а также атрибут id для организации ссылок и автоматической обработки) позволяют теперь яснее помечать отдельные фрагменты документа, придавая им особый характер, равно как и управлять их внешним видом. Эти возможности придали тегу <div> новый смысл и стимулировали его использование.

Присваивая атрибутам id и class имена в разных секциях документа, разграниченных тегами <div id=name class=name> (так поступают и с другими тегами, например с <p>), вы не только помечаете их для последующего обращения к ним при помощи гиперссылок или для автоматической обработки и поддержки (в частности, составления списка библиографических данных по разделам), но можете также определить явно различающиеся стили для этих частей документа. К примеру, можно ввести класс разделов, содержащих аннотацию к документу (скажем, <div class=abstract>), другой класс – для основного текста, третий – для заключения и четвертый – для библиографии (<div class=biblio>).

Затем каждому классу может быть присвоен собственный способ отображения как на уровне документа, так и с помощью внешней присоединенной таблицы стилей: аннотация выводится с отступом и курсивом (скажем, div.abstract {left-margin: +0.5in; font-style: italic}); основной текст – выровненным по левому краю прямым шрифтом; заключение – так же, как аннотация; библиография – с применением автоматической нумерации и подходящим образом отформатированная.

Добавлено: 22 Июля 2018 19:23:47 Добавил: Андрей Ковальчук

Хранение двоичных данных в строках<br />

Задача
Необходимо проанализировать строку, которая содержит значения, закодированные с помощью двоичной структуры, или закодировать значения в строку. Например, нужно сохранить числа в их двоичном представлении, а не как последовательность ASCII-символов.

Решение
Для сохранения двоичных данных в строке применяется функция pack():

$packed = pack('S4',1974,106,28225,32725);


Функция unpack() позволяет извлекать двоичные данные из строки:

$nums = unpack('S4',$packed);


Обсуждение
Первым аргументом функции pack() является строка формата, который описывает способ кодировки данных, передаваемых остальными аргументами. Строка формата S4 указывает, что функция pack() должна сформировать из входных данных четыре беззнаковых коротких целых (short) 16-битных числа в соответствии с машинным порядком
байтов. В качестве входа даны числа 1974, 106, 28225 и 32725, а возвращаются восемь байт: 182, 7, 106, 0, 65, 110, 213 и 127. Каждая двухбайтная пара соответствует входному числу: 7 × 256 + 182 = 1974; 0 × 256 + 106 = 106; 110 × 256 + 65 = 28225; 127 × 256 + 213 = 32725. Первый аргумент функции unpack() – это тоже строка формата, а второй аргумент представляет декодируемые данные. В результате передачи строки формата, равной S4, восьмибайтная последовательность, произведенная функцией pack(), приводит к получению четырехэлементного массива исходных чисел:

print_r($nums);
Array
(
     => 1974
     => 106
     => 28225
     => 32725
)


В функции unpack() за символом форматирования и его множителем может следовать строка, которая выступает в качестве индекса массива. Например:

$nums = unpack('S4num',$packed);
print_r($nums);
Array
(
    [num1] => 1974
    [num2] => 106
    [num3] => 28225
    [num4] => 32725
)


В функции unpack() несколько символов форматирования должны разделяться символом /:

$nums = unpack('S1a/S1b/S1c/S1d',$packed);
print_r($nums);
Array
(
    [a] => 1974
    [b] => 106
    [c] => 28225
    [d] => 32725
)


В табл. 1.2 приведены символы форматирования, которые можно использовать в функциях pack() и unpack().

Таблица 1.2. Символы форматирования функций pack() и unpack()

Символ форматирования | Тип данных
a Строка, дополненная символами NUL
A Строка, дополненная пробелами
h Шестнадцатеричная строка, первый полубайт младший
H Шестнадцатеричная строка, первый полубайт старший
c signed char
C unsigned char
s signed short (16 бит, машинный порядок байтов)
S unsigned short (16 бит, машинный порядок байтов)
n unsigned short (16 бит, обратный порядок байтов)
v unsigned short (16 бит, прямой порядок байтов)
i signed int (машинно-зависимый размер и порядок байтов)
I unsigned int (машинно-зависимый размер и порядок байтов)
l signed long (32 бита, машинный порядок байтов)
L unsigned long (32 бита, машинный порядок байтов)
N unsigned long (32 бита, обратный порядок байтов)
V unsigned long (32 бита, прямой порядок байтов)
f float (машинно-зависимый размер и представление)
d double (машинно-зависимый размер и представление)
x NUL-байт
X Возврат на один байт
@ Заполнение нулями по абсолютному адресу

Для a, A, h и H число после символа форматирования означает длину строки. Например, A25 означает строку из 25 символов, дополненную пробелами. В случае других символов форматирования это число означает количество данных указанного типа, последовательно появляющихся в строке. Остальные возможные данные можно указать при помощи символа «*».
С помощью функции unpack() можно преобразовывать различные типы данных. Этот пример заполняет массив ASCII-кодами каждого символа, находящегося в $s:

$s = 'platypus';
$ascii = unpack('c*',$s);
print_r($ascii);
Array
(
     => 112
     => 108
     => 97
     => 116
     => 121
     => 112
     => 117
     => 115
)

Добавлено: 12 Июля 2018 20:09:53 Добавил: Андрей Ковальчук

Упаковка текста в строки определенной длины

Задача
Необходимо упаковать линии текста в строку. Например, нужно отобразить текст, содержащийся в тегах <pre>/</ pre>, в пределах окна броузера обычного размера.

Решение
Это делается при помощи функции wordwrap():

$s = "Four score and seven years ago our fathers brought forth on this 
continent a new nation, conceived in liberty and dedicated to the proposition 
that all men are created equal.";
print "<pre>\n".wordwrap($s)."\n</pre>";
<pre>
Four score and seven years ago our fathers brought forth on this continent
a new nation, conceived in liberty and dedicated to the proposition that
all men are created equal.
</pre>


Обсуждение
По умолчанию функция wordwrap() упаковывает текст в строки по 75 символов. Необязательный второй аргумент позволяет изменять длину строки:

print wordwrap($s,50);

Four score and seven years ago our fathers brought
forth on this continent a new nation, conceived in
liberty and dedicated to the proposition that all
men are created equal.

Для указания конца строки можно использовать не только символы «\n». Для получения двойного интервала между строками используйте «\n\ n»:

print wordwrap($s,50,"\n\n");

Four score and seven years ago our fathers brought
forth on this continent a new nation, conceived in
liberty and dedicated to the proposition that all
men are created equal.

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

print wordwrap('jabberwocky',5);
print wordwrap('jabberwocky',5,"\n",1);

jabberwocky
jabbe
rwock
y

Добавлено: 12 Июля 2018 20:09:23 Добавил: Андрей Ковальчук

Разбиение строк

Задача
Необходимо разделить строку на части. Например, нужно получить доступ к каждой из строк, которые пользователь вводит в поле <textarea> формы.

Решение
Если в качестве разделителя частей строк выступает строковая константа, то следует применять функцию explode():

$words = explode(' ','My sentence is not very complicated');


Функция split() или функция preg_split() применяются, если при описании разделителя требуется регулярное выражение POSIX или Perl:

$words = split(' +','This sentence  has  some extra whitespace  in it.');
$words = preg_split('/\d\. /','my day: 1. get up 2. get dressed 3. eat toast');
$lines = preg_split('/[\n\r]+/',$_REQUEST['textarea']);


В случае чувствительного к регистру разделителя применяется функция spliti() или флаг /i в функции preg_split():

$words = spliti(' x ','31 inches x 22 inches X 9 inches');
$words = preg_split('/ x /i','31 inches x 22 inches X 9 inches');


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

$dwarves = 'dopey,sleepy,happy,grumpy,sneezy,bashful,doc';
$dwarf_array = explode(',',$dwarves);


Теперь переменная $dwarf_array – это массив из семи элементов:

print_r($dwarf_array);
Array
(
    [0] => dopey
     => sleepy
     => happy
     => grumpy
     => sneezy
     => bashful
     => doc
)


Если заданный предел меньше количества возможных частей, то последняя часть содержит все остальное:

$dwarf_array = explode(',',$dwarves,5);
print_r($dwarf_array);
Array
(
    [0] => dopey
     => sleepy
     => happy
     => grumpy
     => sneezy,bashful,doc
)


Функция explode() трактует разделитель строки буквально. Если разделитель строки определяется как запятая с пробелом, то данная функция делит строку по пробелу, следующему за запятой, а не по запятой или пробелу. Функция split() предоставляет большую гибкость. Вместо строкового литерала в качестве разделителя она использует регулярное выражение POSIX:

$more_dwarves = 'cheeky,fatso, wonder boy, chunky,growly, groggy, winky';
$more_dwarf_array = split(', ?',$more_dwarves);


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

print_r($more_dwarf_array);
Array
(
    [0] => cheeky
     => fatso
     => wonder boy
     => chunky
     => growly
     => groggy
     => winky
)


Существует функция preg_split(), которая подобно функции split() использует Perl-совместимые регулярные выражения вместо регулярных выражений POSIX. Функция preg_split() предоставляет преимущества различных расширений регулярных выражений в Perl, а также хитрые приемы, такие как включение текста-разделителя в возвращаемый массив строк:

$math = "3 + 2 / 7 - 9";
$stack = preg_split('/ *([+\-\/*]) */',$math,-1,PREG_SPLIT_DELIM_CAPTURE);
print_r($stack);
Array
(
    [0] => 3
     => +
     => 2
     => /
     => 7
     => -
     => 9
)


Разделитель-регулярное выражение ищет математические операторы (+, -, /, *), окруженные необязательными начальными или завершающими пробелами. Флаг PREG_SPLIT_DELIM_CAPTURE приказывает функции preg_split() включить совпадения как часть разделителя-регулярного выражения, заключенного в кавычки, в возвращаемый строковый массив. В кавычках только символы математических операций, поэтому возвращенный массив не содержит пробелов.

Добавлено: 12 Июля 2018 20:08:31 Добавил: Андрей Ковальчук

Анализ данных, состоящих из полей фиксированной ширины

Задача
Необходимо разбить на части записи фиксированной ширины в строке.

Решение
Это делается при помощи функции substr():

$fp = fopen('fixed-width-records.txt','r') or die ("can't open file");
while ($s = fgets($fp,1024)) {
    $fields = substr($s,0,10);      // первое поле:  
                                                            первые 10 символов строки
    $fields = substr($s,10,5);      // второе поле: 
                                                            следующие 5 символов строки
    $fields = substr($s,15,12);     // третье поле:  
                                                            следующие 12 символов строки
    // функция обработки полей
    process_fields($fields);
}
fclose($fp) or die("can't close file");

Или функции unpack():
$fp = fopen('fixed-width-records.txt','r') or die ("can't open file");
while ($s = fgets($fp,1024)) {
    // ассоциативный массив с ключами "title", "author" и "publication_year"
    $fields = unpack('A25title/A14author/A4publication_year',$s);
    // функция обработки полей
    process_fields($fields);
}
fclose($fp) or die("can't close file");

Обсуждение
Данные, в которых каждому полю выделено фиксированное число символов в строке, могут выглядеть, как этот список книг, названий и дат опубликования:
$booklist=<<<END
Elmer Gantry             Sinclair Lewis1927
The Scarlatti InheritanceRobert Ludlum 1971
The Parsifal Mosaic      Robert Ludlum 1982
Sophie's Choice          William Styron1979
END;

В каждой строке название занимает 25 символов, имя автора – следующие 14 символов, а год публикации – следующие 4 символа. Зная ширину полей, очень просто с помощью функции substr() перенести поля в массив.
$books = explode("\n",$booklist);
for($i = 0, $j = count($books); $i < $j; $i++) {
  $book_array[$i]['title'] = substr($books[$i],0,25);
  $book_array[$i]['author'] = substr($books[$i],25,14);
  $book_array[$i]['publication_year'] = substr($books[$i],39,4);
}

Разбиение переменной $booklist на массив строк позволяет применить один код разбора одной строки ко всем строкам, прочитанным из файла. Цикл можно сделать более гибким, определив отдельные массивы для имен полей и их ширины, которые могут быть переданы в анализирующую функцию, как показано в функции pc_fixed_width_substr() примера 1.3.

Пример 1.3. pc_fixed_width_substr()
function pc_fixed_width_substr($fields,$data) {
  $r = array();
  for ($i = 0, $j = count($data); $i < $j; $i++) {
    $line_pos = 0;
    foreach($fields as $field_name => $field_length) {
      $r[$i][$field_name] = rtrim(substr($data[$i],$line_pos,$field_length));
      $line_pos += $field_length;
    }
}
  Return $r;
}
$book_fields = array('title' => 25,
                     'author' => 14,
                     'publication_year' => 4);
$book_array = pc_fixed_width_substr($book_fields,$books);

Переменная $line_pos отслеживает начало каждого поля, и она увеличивается на ширину предыдущего поля по мере того, как код обрабатывает каждую строку. Для удаления пробельных символов в конце каждого поля предназначена функция rtrim().
Как альтернатива функции substr() для извлечения полей может применяться функция unpack(). Вместо того чтобы задавать имена полей и их ширину в виде ассоциативных массивов, создайте строку форматирования для функции. Код для извлечения полей фиксированной ширины аналогичен функции pc_fixed_width_unpack(), показанной в примере 1.4.

Пример 1.4. pc_fixed_width_unpack()
function pc_fixed_width_unpack($format_string,$data) {
  $r = array();
  for ($i = 0, $j = count($data); $i < $j; $i++) {
    $r[$i] = unpack($format_string,$data[$i]);
  }
  return $r;
}
$book_array = pc_fixed_width_unpack('A25title/A14author/A4publication_year',
                                    $books);

Формат A означает «строку в обрамлении пробелов», поэтому нет необходимости удалять завершающие пробелы с помощью функции rtrim(). Поля, перенесенные с помощью какой-либо функции в переменную $book_array, могут быть отображены в виде HTML-таблицы, например:
$book_array = pc_fixed_width_unpack('A25title/A14author/A4publication_year',
                                    $books);
print "<table>\n";
// печатаем строку заголовка
print '<tr><td>';
print join('</td><td>',array_keys($book_array[0]));
print "</td></tr>\n";
// печатаем каждую строку данных
foreach ($book_array as $row) {
    print '<tr><td>';
    print join('</td><td>',array_values($row));
    print "</td></tr>\n";
}
print '</table>\n';

Объединение данных с помощью тегов </td><td> формирует строку таблицы, не включая в нее начальный <td> и заключительный </td> теги. Печатая <tr><td> перед выводом объединенных данных и </td></tr> вслед за выводом объединенных данных, мы формируем полную строку таблицы. И функция substr(), и функция unpack() имеют одинаковые возможности, если поля фиксированной ширины содержат строки, но функция unpack() является наилучшим решением при наличии полей других типов данных.

Добавлено: 12 Июля 2018 20:07:34 Добавил: Андрей Ковальчук

Анализ данных, разделенных запятой

Задача
Есть данные, разделенные запятыми (формат CSV), например, файл, экспортированный из Excel или из базы данных, и необходимо извлечь записи и поля в формате, с которым можно работать в PHP.

Решение
Если CSV-данные представляют собой файл (или они доступны через URL), то откройте файл с помощью функции fopen() и прочитайте данные с помощью функции fgetcsv(). Данные будут представлены в виде HTML-таблицы:

$fp = fopen('sample2.csv','r') or die("can't open file");
print "<table>\n";
while($csv_line = fgetcsv($fp,1024)) {
    print '<tr>';
    for ($i = 0, $j = count($csv_line); $i < $j; $i++) {
        print '<td>'.$csv_line[$i].'</td>';
}
    print "</tr>\n";
}
print '</table>\n';
fclose($fp) or die("can't close file");


Обсуждение
Второй аргумент в fgetcsv() должен превышать максимальную длину строки в вашем CSV-файле. (Не забудьте посчитать пробельные символы, ограничивающие строку.) Если длина читаемой строки превышает 1 Kбайт, то число 1024, использованное в данном рецепте, надо заменить на действительную длину строки. Функции fgetcsv() можно передать необязательный третий параметр, ограничитель, используемый вместо запятой. Однако применение другого ограничителя до некоторой степени лишает смысла CSV как наиболее простого способа обмена табличными данными.

Не старайтесь избегать функции fgetcsv(), а просто читайте строку и вызывайте функцию explode() в случае запятых. CSV является более сложным, когда имеешь дело с внедренными запятыми и двойными кавычками. Использование функции fgetcsv() защитит ваш код от трудноуловимых ошибок.

Добавлено: 12 Июля 2018 20:07:04 Добавил: Андрей Ковальчук

Удаление пробельных символов из строки<br />

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

Решение
Следует обратиться к функциям ltrim(), rtrim() или trim(). Функция ltrim() удаляет пробельные символы в начале строки, rtrim() – в конце строки, а функция trim() – и в начале, и в конце строки:

$zipcode = trim($_REQUEST['zipcode']);
$no_linefeed = rtrim($_REQUEST['text']);
$name = ltrim($_REQUEST['name']);


Обсуждение
Эти функции считают пробельными следующие символы: символ новой строки, возврат каретки, пробел, горизонтальную и вертикальную табуляции и символ NULL. Удаление пробельных символов из строки позволяет сэкономить память и может сделать более корректным отображение форматированных данных или текста, например, содержащегося в тегах <pre>. При проверке пользовательского ввода сначала нужно обрезать пробелы, так чтобы не заставлять того, кто ввел «98052 » вместо своего почтового индекса, исправлять ошибку, которая, собственно, таковой не является. Отбрасывание начальных и конечных пробелов в тексте перед его проверкой означает, например, что «salami\n» будет равно «salami».

Неплохо также нормализовать данные путем обрезания пробелов перед их занесением в базу данных.
Кроме того, функция trim() способна удалять из строки символы, определенные пользователем. Удаляемые символы передаются в качестве второго аргумента. Можно указать интервал символов с помощью двоеточия между первым и последним символом интервала.

// Удаление цифр и пробела в начале строки
print ltrim('10 PRINT A$',' 0..9');
// Удаление точки с запятой в конце строки
print rtrim('SELECT * FROM turtles;',';');
PRINT A$
SELECT * FROM turtles


PHP рассматривает chop() как синоним rtrim(). Однако лучше использовать rtrim(), поскольку поведение функции chop() в PHP отличается от поведения chop() в Perl (вместо которой в любом случае лучше применять функцию chomp()), а ее применение может затруднить другим чтение вашего кода.

Добавлено: 12 Июля 2018 20:06:36 Добавил: Андрей Ковальчук

Включение функций и выражений в строки

Задача
Вставить результаты выполнения функции или выражения в строку.

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

print 'You have '.($_REQUEST['boys'] + $_REQUEST['girls']).' children.';
print "The word '$word' is ".strlen($word).' characters long.';
print 'You owe '.$amounts['payment'].' immediately';
print "My circle's diameter is ".$circle->getDiameter().' inches.';


Обсуждение
Можно поместить переменные, свойства объекта и элементы массива (если индекс не в кавычках) непосредственно в строку в двойных кавычках:

print "I have $children children.";
print "You owe $amounts[payment] immediately.";
print "My circle's diameter is $circle->diameter inches.";


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

print <<< END
Right now, the time is 
END
. strftime('%c') . <<< END
but tomorrow it will be 
END
. strftime('%c',time() + 86400);


Кроме того, если вы производите вставку во встроенный документ, не забудьте добавить пробелы так, чтобы вся строка выглядела правильно. В предыдущем примере строка «Right now the time» должна включать замыкающий пробел, а строка «but tomorrow it will be» должна включать пробелы в начале и в конце.

Добавлено: 12 Июля 2018 20:06:10 Добавил: Андрей Ковальчук

Управление регистром

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

Решение
Первые буквы одного или более слов можно сделать прописными с помощью функции ucfirst() или функции ucwords():

print ucfirst("how do you do today?");
print ucwords("the prince of wales");

How do you do today?
The Prince Of Wales

Регистр всей строки изменяется функцией strtolower() или функцией strtoupper():

print strtoupper("i'm not yelling!");
// Стандарт XHTML требует, чтобы символы в тегах были в нижнем регистре
print strtolower('<A HREF="one.php">one</A>');

I'M NOT YELLING!
one

Обсуждение
Первый символ строки можно сделать прописным посредством функции ucfirst():

print ucfirst('monkey face');
print ucfirst('1 monkey face');

Monkey face
1 monkey face

Обратите внимание, что во второй строке вывода слово «monkey» начинается со строчной буквы. Функция ucwords() позволяет сделать прописным первый символ каждого слова в строке:

print ucwords('1 monkey face');
print ucwords("don't play zone defense against the philadelphia 76-ers");

1 Monkey Face
Don't Play Zone Defense Against The Philadelphia 76-ers

Как и следовало ожидать, функция ucwords() не делает прописной букву «t» в слове «don’t». Но она также не делает прописной букву «е» в «70-е». Для функции ucwords() слово – это любая последовательность непробельных символов, за которой расположен один или несколько пробельных. Символы «'» и «-» не являются пробельными, поэтому функция ucwords() не считает «t» в «don’t» или «е» в «70-е» начальными символами слов. Ни ucfirst(), ни ucwords() не изменяют регистр не первых символов:

print ucfirst('macWorld says I should get a iBook');
print ucwords('eTunaFish.com might buy itunaFish.Com!');

MacWorld says I should get a iBook
ETunaFish.com Might Buy ItunaFish.Com!

Функции strtolower() и strtoupper() работают с целыми строками, а не только с отдельными символами. Функция strtolower() переводит все алфавитные символы в нижний регистр, а функция strtoupper() – в верхний:

print strtolower("I programmed the WOPR and the TRS-80.");
print strtoupper('"since feeling is first" is a poem by e. e. cummings.');

i programmed the wopr and the trs-80.
"SINCE FEELING IS FIRST" IS A POEM BY E. E. CUMMINGS.

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

Добавлено: 12 Июля 2018 20:05:39 Добавил: Андрей Ковальчук

Расширение и сжатие табуляций

Задача
Нужно заменить пробелы на табуляцию (или табуляцию на пробелы) и в то же время сохранить выравнивание теста по позициям табуляции. Например, вы хотите отобразить для пользователя текст стандартным образом.

Решение
Для замены пробелов на табуляцию или табуляции на пробелы следует применять функцию str_replace():

$r = mysql_query("SELECT message FROM messages WHERE id = 1") or die();
$ob = mysql_fetch_object($r);
$tabbed = str_replace(' ',"\t",$ob->message);
$spaced = str_replace("\t",' ',$ob->message);
print "With Tabs: <pre>$tabbed</pre>";
print "With Spaces: <pre>$spaced</pre>";


Однако если для преобразования применяется функция str_replace(), то позиции табуляции нарушаются. Если вы хотите ставить табуляцию через каждые восемь символов, то в строке, начинающейся с пятибуквенного слова и табуляции, необходимо заменить табуляцию на три пробела, а не на один. Для замены табуляции на пробелы с учетом
позиций табуляции следует применять функцию pc_tab_expand(), показанную в примере 1.1.

Пример 1.1. pc_tab_expand()
function pc_tab_expand($a) {
  $tab_stop = 8;
  while (strstr($a,"\t")) {
    $a = preg_replace('/^([^\t]*)(\t+)/e',
                      "'\\1'.str_repeat(' ',strlen('\\2') * 
                       $tab_stop - strlen('\\1') % $tab_stop)",$a);
  } 
  return $a;
}
$spaced = pc_tab_expand($ob->message);


Для обратной замены пробелов на табуляцию можно воспользоваться функцией pc_tab_unexpand(), показанной в примере 1.2.

Пример 1.2. pc_tab_unexpand()
function pc_tab_unexpand($x) {
  $tab_stop = 8;
  $lines = explode("\n",$x);
  for ($i = 0, $j = count($lines); $i < $j; $i++) {
    $lines[$i] = pc_tab_expand($lines[$i]);
    $e = preg_split("/(.\{$tab_stop})/",$lines[$i],-
1,PREG_SPLIT_DELIM_CAPTURE);
    $lastbit = array_pop($e);
    if (!isset($lastbit)) { $lastbit = ''; }
    if ($lastbit == str_repeat(' ',$tab_stop)) { $lastbit = "\t"; }
    for ($m = 0, $n = count($e); $m < $n; $m++) {
      $e[$m] = preg_replace('/  +$',"\t",$e[$m]);
    }
$lines[$i] = join('',$e).$lastbit;
  }
  $x = join("\n", $lines);
  return $x;
}
$tabbed = pc_tab_unexpand($ob->message);


Обе функции принимают в качестве аргумента строку и возвращают ее, модифицировав соответствующим образом.

Обсуждение
Каждая функция предполагает наличие позиций табуляции через каждые восемь пробелов, но это можно изменить, задав переменную $tab_stop. Регулярное выражение в pc_tab_expand() соответствует и группе табуляций, и всему тексту в строке перед группой табуляций. Оно должно соответствовать тексту перед табуляциями, поскольку от длины этого текста зависит количество пробелов, замещающих табуляции, а последующий текст должен быть выровнен по позиции следующей табуляции. Эта функция не просто заменяет каждую табуляцию на восемь пробелов; она выравнивает текст, стоящий после табуляции, по позициям табуляций.

Точно так же функция pc_tab_unexpand() не только ищет восемь последовательных пробелов, а затем заменяет их одним символом табуляции. Она делит каждую строку на участки по восемь символов, а затем замещает пробелы в конце этих участков (по крайней мере два пробела) на табуляции. Это не только сохраняет выравнивание текста по позициям табуляций, но и сохраняет пробелы в строке.

Добавлено: 12 Июля 2018 20:04:58 Добавил: Андрей Ковальчук

Пословный или посимвольный переворот строки

Задача
Требуется перевернуть слова или символы в строке.

Решение
Для посимвольного переворота строки применяется функция strrev():

print strrev('This is not a palindrome.');

.emordnilap a ton si sihT

Чтобы перевернуть строку пословно, надо разобрать строку на слова, перевернуть слова, а затем собрать их заново в строку:

$s = "Once upon a time there was a turtle.";
// разбиваем строку на слова
$words = explode(' ',$s);
// обращаем массив слов
$words = array_reverse($words);
// $s = join(' ',$words);
print $s;

turtle. a was there time a upon Once

Обсуждение
Пословное обращение строки может быть также выполнено в одной строке:

$reversed_s = join(' ',array_reverse(explode(' ',$s)))

Добавлено: 12 Июля 2018 20:03:59 Добавил: Андрей Ковальчук

Посимвольная обработка строк

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

Решение
Цикл по символам строки с помощью оператора for. В этом примере подсчитываются гласные в строке:

$string = "This weekend, I'm going shopping for a pet chicken.";
$vowels = 0;
for ($i = 0, $j = strlen($string); $i < $j; $i++) {
    if (strstr('aeiouAEIOU',$string[$i])) {
        $vowels++;
    }
}


Обсуждение
Посимвольная обработка – это самый простой способ подсчета последовательности «Смотри и говори»:

function lookandsay($s) {
    // инициализируем возвращаемое значение пустой строкой
    $r = '';
    // переменная $m, которая содержит подсчитываемые символы, 
// инициализируется первым символом * в строке
    $m = $s[0];
    // $n, количество обнаруженных символов $m, инициализируется значением 1
    $n = 1;
    for ($i = 1, $j = strlen($s); $i < $j; $i++) {
        // если символ совпадает с последним символом
        if ($s[$i] == $m) {
            // увеличиваем на единицу значение счетчика этих символов
            $n++;
        } else {
            // иначе добавляем значение счетчика и символа 
               к возвращаемому значению //
            $r .= $n.$m;
            // устанавливаем искомый символ в значение текущего символа //
            $m = $s[$i];
            // и сбрасываем счетчик в 1 //
            $n = 1;
        }
    }
    // возвращаем построенную строку, а также последнее значение 
       счетчика и символ //
    return $r.$n.$m;
}
for ($i = 0, $s = 1; $i < 10; $i++) {
    $s = lookandsay($s);
    print "$s\n";
}

1
11
21
1211
111221
312211
13112221
1113213211
31131211131221
13211311123113112211

Это называется последовательностью «Смотри и говори», поскольку каждый элемент мы получаем, глядя на предыдущие элементы и говоря, сколько их. Например, глядя на первый элемент, 1, мы говорим «один». Следовательно, второй элемент – «11», то есть две единицы, поэтому третий элемент – «21». Он представляет собой одну двойку и одну единицу, поэтому четвертый элемент – «1211» и т. д.

Добавлено: 12 Июля 2018 19:44:19 Добавил: Андрей Ковальчук

Определение кодировки страницы сайта.

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

Представленный ниже пример читает страницу, преобразует её в UTF-8, загружает в DOM-объект, получает из него title и выводит его в кодировке windows-1251.

<form method=\"get\">
Страница для анализа: <input type=\"text\" name=\"url\">
</form>

PHP анализ и обработка:
<?php
$url=@$_GET['url'];
echo "<br>Страница <b>".$url."</b><br>\n";
   $curl = curl_init($url);
   curl_setopt($curl, CURLOPT_RETURNTRANSFER,true);
   curl_setopt($curl, CURLOPT_HEADER, 1);    // включать header в вывод
   curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);    // следовать любому "Location: " header
   curl_setopt($curl, CURLOPT_TIMEOUT, 20);    // максимальное время в секундах, для работы CURL-функций.
   $html = @curl_exec($curl);

   $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
   curl_close($curl); // close cURL handler
   $headers = substr($html, 0, $header_size - 4)."\n";
   $body = substr($html, $header_size);
if (preg_match("|Content-Type: .*?charset=(.*)\n|imsU", $headers, $results))$ct0=trim($results[1]);
else $ct0=false;
if (preg_match_all('/(<meta\s*http-equiv=[\'\"]Content-Type[\'\"]\s*content=[\'\"][^;]*;\s*charset=([^\"\']*?)(?:"|\;|\')[^>]*>)/i',$body,$arr,PREG_PATTERN_ORDER)){
    $ct1=strtolower(trim($arr[2][0]));
    // не учитываю, что заголовки могут быть разными. Это в платной версии.
    if ($ct1=='utf-8')$ct1=false;
    $ct0=false; // meta не добавлять
}else $ct1=false;

if ($ct1){
    echo "<br>Преобразование ".$ct1." -> UTF-8";
    $body=@iconv($ct1,'utf-8//IGNORE',$body);
}

// добавляю в head Content-Type
if($ct0)$body=preg_replace('/<head[^>]*>/','<head>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
',$body);

$doc = new DOMDocument();

@$doc->loadHTML($body);

if ( ($tags = @$doc->getElementsByTagName('title')) ) {
    $title = @$tags->item(0)->nodeValue;
    print "<br>Заголовок страницы: ".@iconv("UTF-8", "windows-1251//IGNORE", $title).'<br />';
}
?>

Приведенный пример упрощен и не содержит обработку "трудных" случаев, когда кодовая страница в header указанна одна, а в META другая. Также не содержит обработку двойных мета с разными кодовыми страницами(и такое у меня попадалось). Полный пример дополнительно осуществляет кеширование и "борется" с кривыми страницами. Представленный код будет корректно работать на 95% страниц/сайтов в рунете.

Добавлено: 03 Июля 2018 08:40:22 Добавил: Андрей Ковальчук