Как отлаживать запросы к базе данных PDO?


142

Перед тем как перейти к PDO, я создал SQL-запросы в PHP путем объединения строк. Если я получил синтаксическую ошибку базы данных, я мог бы просто повторить последнюю строку запроса SQL, попробовать ее сам в базе данных и настроить ее, пока не исправлю ошибку, а затем вернуть ее в код.

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

Есть ли способ захватить полный SQL-запрос, отправленный PDO в базу данных, и записать его в файл?


4
Он является зарегистрированным в файле: /var/log/mysql/*. Параметры, связанные с PDO, не могут вызывать синтаксических ошибок, поэтому все, что вам нужно, - это подготовленный SQL-запрос.
Xeoncross

1
см. код в stackoverflow.com/questions/210564/… (не в принятом ответе). Не то, чтобы было опубликовано несколько обновлений.
Mawg требует восстановить Монику

1
Простая однострочная строка через Composer: github.com/panique/pdo-debug
Sliq

2
Ответ Xeoncross мне помог. Вот статья, объясняющая, как включить эту функцию. Это отключено по умолчанию на многих серверах. pontikis.net/blog/how-and-when-to-enable-mysql-logs
mrbinky3000

2
Попробуйте сvar_dump($pdo_instance->debugDumpParams())
Даниил Петровалиев 09

Ответы:


102

Вы говорите это:

Я никогда не вижу окончательного запроса, поскольку он отправляется в базу данных

На самом деле, при использовании подготовленных операторов не бывает такого понятия, как « окончательный запрос » :

  • Сначала в БД отправляется инструкция, которая там готовится.
    • База данных анализирует запрос и строит его внутреннее представление.
  • И, когда вы связываете переменные и выполняете оператор, в базу данных отправляются только переменные.
    • И база данных «вставляет» значения во внутреннее представление оператора.


Итак, чтобы ответить на ваш вопрос:

Есть ли способ захватить полный SQL-запрос, отправленный PDO в базу данных, и записать его в файл?

Нет: поскольку нигде нет « полного SQL-запроса », нет и возможности его захватить.


Лучшее, что вы можете сделать для целей отладки, - это «воссоздать» «настоящий» SQL-запрос, вставив значения в строку SQL оператора.

Что я обычно делаю в таких ситуациях:

  • повторить код SQL, соответствующий оператору, с заполнителями
  • и используйте var_dump (или эквивалент) сразу после, чтобы отобразить значения параметров
  • Обычно этого достаточно, чтобы увидеть возможную ошибку, даже если у вас нет «настоящего» запроса, который можно было бы выполнить.

Это не очень хорошо, когда дело доходит до отладки, но это цена подготовленных операторов и преимущества, которые они приносят.


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

Пожалуйста :-) ;;; Я не знаю в деталях, как это реализовано, но я предполагаю, что это что-то вроде этого - в любом случае результат точно такой ;;; это одна из приятных вещей с подготовленными операторами: если вам нужно выполнить один и тот же запрос много раз, он будет отправлен в БД и подготовлен только один раз: для каждого выполнения будут отправляться только данные.
Паскаль МАРТИН

1
Обновление: Аарон Паттерсон упомянул на Railsconf 2011, что он добавил больше подготовленных операторов в Rails, но это преимущество гораздо больше в PostgreSQL, чем в MySQL. Он сказал, что это связано с тем, что MySQL фактически не создает план запроса, пока вы не выполните подготовленный запрос.
Nathan Long

86

Просмотр журнала базы данных

Хотя Паскаль МАРТИН прав в том, что PDO не отправляет полный запрос к базе данных сразу, предложение ryeguy использовать функцию регистрации базы данных фактически позволило мне увидеть полный запрос, собранный и выполненный базой данных.

Вот как: (Эти инструкции предназначены для MySQL на машине с Windows - ваш опыт может отличаться)

  • В my.iniпод [mysqld]разделом добавьте logкоманду, напримерlog="C:\Program Files\MySQL\MySQL Server 5.1\data\mysql.log"
  • Перезапустите MySQL.
  • Он начнет регистрировать каждый запрос в этом файле.

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


1
Просто примечание - мне пришлось избегать косых черт в my.ini. Итак, моя запись выглядела примерно так: log = "C: \\ temp \\ MySQL \\ mysql.log".
Джим

4
Это может работать в зависимости от настройки PDO::ATTR_EMULATE_PREPARES. См. Этот ответ для получения дополнительной информации: stackoverflow.com/questions/10658865/#answer-10658929
webbiedave

24
Я ненавижу PDO из-за этого.
Салман

1
@webbiedave - о, вау! Ваш связанный ответ подразумевает, что мой ответ работает только тогда, когда PDO не работает оптимально, а скорее отправляет весь запрос для обратной совместимости со старой версией MySQL или старым драйвером. Интересно.
Натан Лонг,

13
В MySQL 5.5+ general_logвместо log. См. Dev.mysql.com/doc/refman/5.5/en/query-log.html
Адриан Макнейл,

20

Конечно, вы можете отлаживать в этом режиме. {{ PDO::ATTR_ERRMODE }} Просто добавьте новую строку перед запросом, и вы увидите строки отладки.

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$db->query('SELECT *******');  

Но вы бы не позвонили ->queryпри использовании подготовленных операторов?
EoghanM

Спасибо, это мне очень помогло! :)
разблокировка

Большое спасибо!
Нико Бюттнер,

19

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

Он не создает за вас подготовленный оператор, но он покажет ваши параметры.


2
Единственная проблема заключается в том, что он выводит отладку вместо того, чтобы хранить ее внутри, не повторяя ее. Я не могу зарегистрировать это таким образом.
Рикардо Мартинс,

3
Вы можете использовать буферизацию вывода (ob_start () ...), чтобы сохранить вывод и зарегистрировать его.
Cranio

bugs.php.net/bug.php?id=52384 исправлено в 7.1, вы можете видеть значения :) немного поздно, но это php
Сандер Виссер

12

Старый пост, но, возможно, кому-то это пригодится;

function pdo_sql_debug($sql,$placeholders){
    foreach($placeholders as $k => $v){
        $sql = preg_replace('/:'.$k.'/',"'".$v."'",$sql);
    }
    return $sql;
}

1
Для аналогичной функции, которая также может обрабатывать числовые параметры, см. Мой ответ (спасибо комментатору на php.net).
Мэтт Браун

10

Вот функция, позволяющая увидеть, каким будет эффективный SQL, из комментария «Mark» на php.net :

function sql_debug($sql_string, array $params = null) {
    if (!empty($params)) {
        $indexed = $params == array_values($params);
        foreach($params as $k=>$v) {
            if (is_object($v)) {
                if ($v instanceof \DateTime) $v = $v->format('Y-m-d H:i:s');
                else continue;
            }
            elseif (is_string($v)) $v="'$v'";
            elseif ($v === null) $v='NULL';
            elseif (is_array($v)) $v = implode(',', $v);

            if ($indexed) {
                $sql_string = preg_replace('/\?/', $v, $sql_string, 1);
            }
            else {
                if ($k[0] != ':') $k = ':'.$k; //add leading colon if it was left out
                $sql_string = str_replace($k,$v,$sql_string);
            }
        }
    }
    return $sql_string;
}

Почему "Mark" использует двоеточие перед $ k str_replace(":$k" ....? В ассоциативных индексах он уже есть в массиве $ params.
Алан

Хороший вопрос ... это может объяснить это: stackoverflow.com/questions/9778887/… . Лично я использовал эту функцию для отладки запросов Doctrine, и я думаю, что Doctrine использует нумерованные, а не именованные параметры, поэтому я не заметил этой проблемы. Я обновил функцию, чтобы теперь она работала как с начальными двоеточиями, так и без них.
Мэтт Браун,

обратите внимание, что это решение заменяется :name_longна :name. По крайней мере, если :nameбудет раньше :name_long. Подготовленные операторы MySQL могут справиться с этим правильно, так что пусть это вас не смущает.
Zim84

8

Нет. Запросы PDO не подготавливаются на стороне клиента. PDO просто отправляет SQL-запрос и параметры на сервер базы данных. База данных - это то, что делает замену (из ?). У вас есть два варианта:

  • Используйте функцию ведения журнала своей БД (но даже тогда она обычно отображается как два отдельных оператора (т.е. «не окончательный»), по крайней мере, с Postgres)
  • Выведите SQL-запрос и параметры и сами соберите их вместе.

Никогда не думал проверять журнал БД. Я ковыряюсь в каталоге MySQL и не вижу никаких файлов журналов, но, возможно, ведение журнала - это вариант, который мне нужно где-то включить.
Натан Лонг,

Да, надо его включить. Я не знаю подробностей, но по умолчанию он не регистрирует каждый запрос.
ryeguy 09

6

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

<?php
/* Provoke an error -- bogus SQL syntax */
$stmt = $dbh->prepare('bogus sql');
if (!$stmt) {
    echo "\PDO::errorInfo():\n";
    print_r($dbh->errorInfo());
}
?>

( ссылка на источник )

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


2
Это неправильный путь. PDO достаточно умен, чтобы сделать этот код бесполезным. Просто скажите ему, чтобы он генерировал исключения при ошибках. Все остальное PHP сделает лучше, чем эта ограниченная функция. Также, пожалуйста , научитесь не выводить все ошибки прямо в браузер. Есть способы получше.
Your Common Sense

3
это официальная документация, и, конечно же, никто не собирался печатать эту ошибку в производственной среде, снова это пример с официального сайта (php.net), см. ссылку под примером кода. И наверняка гораздо лучше использовать дополнительные параметры $ db-> setAttribute (PDO :: ATTR_ERRMODE, PDO :: ERRMODE_EXCEPTION) в экземпляре PDO, но, к сожалению, у вас не было доступа к этому коду
Zippp

4

например, у вас есть этот оператор pdo:

$query="insert into tblTest (field1, field2, field3)
values (:val1, :val2, :val3)";
$res=$db->prepare($query);
$res->execute(array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
));

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

$assoc=array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
);
$exQuery=str_replace(array_keys($assoc), array_values($assoc), $query);
echo $exQuery;

1
Работал у меня. У вас есть ошибка во втором примере кода: ));должно быть );(только одна круглая скобка).
Jasom Dotnet

3

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

Итак, в соответствии с этим потоком вы можете написать оболочку для вашего PDO-соединения, которая может регистрировать и генерировать исключение, когда вы получаете ошибку.

Вот простой пример:

class LoggedPDOSTatement extends PDOStatement    {

function execute ($array)    {
    parent::execute ($array);
    $errors = parent::errorInfo();
    if ($errors[0] != '00000'):
        throw new Exception ($errors[2]);
    endif;
  }

}

поэтому вы можете использовать этот класс вместо PDOStatement:

$this->db->setAttribute (PDO::ATTR_STATEMENT_CLASS, array ('LoggedPDOStatement', array()));

Вот упомянутая реализация декоратора PDO:

class LoggedPDOStatement    {

function __construct ($stmt)    {
    $this->stmt = $stmt;
}

function execute ($params = null)    {
    $result = $this->stmt->execute ($params); 
    if ($this->stmt->errorCode() != PDO::ERR_NONE):
        $errors = $this->stmt->errorInfo();
        $this->paint ($errors[2]);
    endif;
    return $result;
}

function bindValue ($key, $value)    {
    $this->values[$key] = $value;    
    return $this->stmt->bindValue ($key, $value);
}

function paint ($message = false)    {
    echo '<pre>';
    echo '<table cellpadding="5px">';
    echo '<tr><td colspan="2">Message: ' . $message . '</td></tr>';
    echo '<tr><td colspan="2">Query: ' . $this->stmt->queryString . '</td></tr>';
    if (count ($this->values) > 0):
    foreach ($this->values as $key => $value):
    echo '<tr><th align="left" style="background-color: #ccc;">' . $key . '</th><td>' . $value . '</td></tr>';
    endforeach;
    endif;
    echo '</table>';
    echo '</pre>';
}

function __call ($method, $params)    {
    return call_user_func_array (array ($this->stmt, $method), $params); 
}

}

2

Чтобы зарегистрировать MySQL в WAMP , вам нужно будет отредактировать my.ini (например, в wamp \ bin \ mysql \ mysql5.6.17 \ my.ini)

и добавить к [mysqld]:

general_log = 1
general_log_file="c:\\tmp\\mysql.log"

2

Вот функция, которую я сделал для возврата SQL-запроса с "разрешенными" параметрами.

function paramToString($query, $parameters) {
    if(!empty($parameters)) {
        foreach($parameters as $key => $value) {
            preg_match('/(\?(?!=))/i', $query, $match, PREG_OFFSET_CAPTURE);
            $query = substr_replace($query, $value, $match[0][1], 1);
        }
    }
    return $query;
    $query = "SELECT email FROM table WHERE id = ? AND username = ?";
    $values = [1, 'Super'];

    echo paramToString($query, $values);

Предполагая, что вы выполняете это так

$values = array(1, 'SomeUsername');
$smth->execute($values);

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


Я добавил это:if (is_string($value)) { $value = "'".$value."'"; }
Jurgen Dictus

0

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

/**
 * Executes a line of sql with PDO.
 * 
 * @param string $sql
 * @param array $params
 */
class TableModel{
    var $_db; //PDO connection
    var $_query; //PDO query

    function execute($sql, $params) { 
        //we're saving this as a global, so it's available to the error handler
        global $_tm;
        //setting these so they're available to the error handler as well
        $this->_sql = $sql;
        $this->_paramArray = $params;            

        $this->_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->_query = $this->_db->prepare($sql);

        try {
            //set a custom error handler for pdo to catch any php errors
            set_error_handler('pdoErrorHandler');

            //save the table model object to make it available to the pdoErrorHandler
            $_tm = $this;
            $this->_query->execute($params);

            //now we restore the normal error handler
            restore_error_handler();
        } catch (Exception $ex) {
            pdoErrorHandler();
            return false;
        }            
    }
}

Таким образом, приведенный выше код перехватывает ОБЕИ исключения PDO И синтаксические ошибки php и обрабатывает их одинаково. Мой обработчик ошибок выглядит примерно так:

function pdoErrorHandler() {
    //get all the stuff that we set in the table model
    global $_tm;
    $sql = $_tm->_sql;
    $params = $_tm->_params;
    $query = $tm->_query;

    $message = 'PDO error: ' . $sql . ' (' . implode(', ', $params) . ") \n";

    //get trace info, so we can know where the sql call originated from
    ob_start();
    debug_backtrace(); //I have a custom method here that parses debug backtrace, but this will work as well
    $trace = ob_get_clean();

    //log the error in a civilized manner
    error_log($message);

    if(admin(){
        //print error to screen based on your environment, logged in credentials, etc.
        print_r($message);
    }
}

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


0

этот код отлично работает для меня:

echo str_replace(array_keys($data), array_values($data), $query->queryString);

Не забудьте заменить $ data и $ query своими именами


0

я использую этот класс для отладки PDO (с Log4PHP )

<?php

/**
 * Extends PDO and logs all queries that are executed and how long
 * they take, including queries issued via prepared statements
 */
class LoggedPDO extends PDO
{

    public static $log = array();

    public function __construct($dsn, $username = null, $password = null, $options = null)
    {
        parent::__construct($dsn, $username, $password, $options);
    }

    public function query($query)
    {
        $result = parent::query($query);
        return $result;
    }

    /**
     * @return LoggedPDOStatement
     */
    public function prepare($statement, $options = NULL)
    {
        if (!$options) {
            $options = array();
        }
        return new \LoggedPDOStatement(parent::prepare($statement, $options));
    }
}

/**
 * PDOStatement decorator that logs when a PDOStatement is
 * executed, and the time it took to run
 * @see LoggedPDO
 */
class LoggedPDOStatement
{

    /**
     * The PDOStatement we decorate
     */
    private $statement;
    protected $_debugValues = null;

    public function __construct(PDOStatement $statement)
    {
        $this->statement = $statement;
    }

    public function getLogger()
    {
        return \Logger::getLogger('PDO sql');
    }

    /**
     * When execute is called record the time it takes and
     * then log the query
     * @return PDO result set
     */
    public function execute(array $params = array())
    {
        $start = microtime(true);
        if (empty($params)) {
            $result = $this->statement->execute();
        } else {
            foreach ($params as $key => $value) {
                $this->_debugValues[$key] = $value;
            }
            $result = $this->statement->execute($params);
        }

        $this->getLogger()->debug($this->_debugQuery());

        $time = microtime(true) - $start;
        $ar = (int) $this->statement->rowCount();
        $this->getLogger()->debug('Affected rows: ' . $ar . ' Query took: ' . round($time * 1000, 3) . ' ms');
        return $result;
    }

    public function bindValue($parameter, $value, $data_type = false)
    {
        $this->_debugValues[$parameter] = $value;
        return $this->statement->bindValue($parameter, $value, $data_type);
    }

    public function _debugQuery($replaced = true)
    {
        $q = $this->statement->queryString;

        if (!$replaced) {
            return $q;
        }

        return preg_replace_callback('/:([0-9a-z_]+)/i', array($this, '_debugReplace'), $q);
    }

    protected function _debugReplace($m)
    {
        $v = $this->_debugValues[$m[0]];

        if ($v === null) {
            return "NULL";
        }
        if (!is_numeric($v)) {
            $v = str_replace("'", "''", $v);
        }

        return "'" . $v . "'";
    }

    /**
     * Other than execute pass all other calls to the PDOStatement object
     * @param string $function_name
     * @param array $parameters arguments
     */
    public function __call($function_name, $parameters)
    {
        return call_user_func_array(array($this->statement, $function_name), $parameters);
    }
}

0

Я создал современный проект / репозиторий, загруженный Composer, именно для этого:

pdo-debug

Найдите здесь GitHub-главную страницу проекта , посмотрите сообщение в блоге, объясняющее это здесь . Одна строка, которую нужно добавить в свой composer.json, а затем вы можете использовать ее следующим образом:

echo debugPDO($sql, $parameters);

$ sql - это необработанный оператор SQL, $ parameters - это массив ваших параметров: ключ - это имя-заполнитель (": user_id") или номер безымянного параметра ("?"), значение ... ну, значение.

Логика, лежащая в основе: этот сценарий просто градуирует параметры и заменяет их предоставленной строкой SQL. Супер-простой, но суперэффективный для 99% случаев использования. Примечание: это просто базовая эмуляция, а не настоящая отладка PDO (поскольку это невозможно, поскольку PHP отправляет необработанный SQL и параметры на сервер MySQL отдельно).

Большое спасибо к bigwebguy и Майку из нити StackOverflow Получения строки сырой SQL запроса из PDO для записи в основном все основные функции за этот сценарием. Большой!


0

Как отлаживать PDO запросы к базе данных mysql в Ubuntu

TL; DR Регистрируйте все ваши запросы и следите за журналом mysql.

Эти инструкции относятся к моей установке Ubuntu 14.04. Выполните команду, lsb_release -aчтобы получить свою версию. Ваша установка может отличаться.

Включите вход в mysql

  1. Перейдите в строку cmd вашего сервера dev
  2. Сменить каталоги cd /etc/mysql. Вы должны увидеть файл с именемmy.cnf . Это файл, который мы собираемся изменить.
  3. Убедитесь, что вы находитесь в нужном месте, набрав cat my.cnf | grep general_log. Это фильтрует my.cnfфайл за вас. Вы должны увидеть две записи: #general_log_file = /var/log/mysql/mysql.log&& #general_log = 1.
  4. Раскомментируйте эти две строки и сохраните их в любом редакторе.
  5. Перезапуск MySQL: sudo service mysql restart.
  6. Возможно, вам также потребуется перезапустить ваш веб-сервер. (Я не могу вспомнить последовательность, которую использовал). Для моей установки, это Nginx: sudo service nginx restart.

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

Следите за журналом, чтобы увидеть свои запросы

Введите этот cmd tail -f /var/log/mysql/mysql.log.

Ваш результат будет выглядеть примерно так:

73 Connect  xyz@localhost on your_db
73 Query    SET NAMES utf8mb4
74 Connect  xyz@localhost on your_db
75 Connect  xyz@localhost on your_db
74 Quit 
75 Prepare  SELECT email FROM customer WHERE email=? LIMIT ?
75 Execute  SELECT email FROM customer WHERE email='a@b.co' LIMIT 5
75 Close stmt   
75 Quit 
73 Quit 

Любые новые запросы, которые делает ваше приложение, будут автоматически появляться в поле зрения , пока вы продолжаете отслеживать журнал. Чтобы выйти из хвоста, нажмитеcmd/ctrl c .

Примечания

  1. Осторожно: этот файл журнала может стать огромным. Я запускаю это только на своем сервере разработки.
  2. Файл журнала становится слишком большим? Обрежьте это. Это означает, что файл остается, но его содержимое удаляется. truncate --size 0 mysql.log.
  3. Классно, что в файле журнала перечислены подключения mysql. Я знаю, что один из них взят из моего устаревшего кода mysqli, с которого я перехожу. Третий - от моего нового подключения PDO. Однако не уверен, откуда взялось второе. Если вы знаете, как быстро его найти, дайте мне знать.

Кредит и спасибо

Огромный привет ответу Натана Лонга выше за вдохновение, чтобы понять это на Ubuntu. Также к дикириллу за его комментарий к сообщению Натана, который привел меня к этому решению.

Люблю тебя, stackoverflow!


0

В среде Debian NGINX я сделал следующее.

Перейти к /etc/mysql/mysql.conf.dредактированию, mysqld.cnfесли вы обнаружите, что log-error = /var/log/mysql/error.logдобавьте следующие две строки под ним.

general_log_file        = /var/log/mysql/mysql.log
general_log             = 1

Чтобы увидеть журналы goto /var/log/mysqlи tail -f mysql.log

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.