Magento. Пишем свой модуль. Добавляем CAPTCHA и дополнительные поля в регистрацию

    Хочу поделится опытом программирования модулей для Magento. Большое спасибо пользователю jeje за приглашение.
    В статье подробно описано создание модуля, реализующего допольнительные функции регистрации клиентов. Цели — дать представление о разработке под Magento на конкретном примере от начала до конца, показать основные подходы, организацию кода, указать на некоторые особенности. Статья ориентирована скорее на новичков, но и знакомые с Magento могут вынести что-то полезное для себя. Конечно в одной статье всего описать невозможно, но если тема окажется востребованной, то это может стать началом цикла статей.
    Затронуты следующие моменты:
    • создание модуля
    • работа с блоками, шаблонами и разметкой (layout)
    • переопределение контроллера
    • скрипты инсталляции модуля

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



    Страница регистрации

    Чего задумали


    Есть страница регистрации /magento/customer/account/create. Мы хотим добавить проверку CAPTCHA, а также спрашивать у клиентов группу и код приглашения. Полученные данные сохранять в базу. По умолчанию в Magento определены три группы клиентов General, Wholesale и Retailer. Из них мы и предложим выбирать будущему клиенту. Про код приглашения Magento ничего не знает — создадим новый атрибут.

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

    Поиск


    Ищем где лежат файлы шаблонов, где и как сохраняются данные регистрации.
    1. Шаблоны формы регистрации клиента
      /magento/app/design/frontend/default/default/template/customer/form/register.phtml — анкета регистрации
    2. Атрибуты добавляют в инсталляторе модуля, пример:
      /magento/app/code/core/Mage/Customer/sql/customer_setup/mysql4-upgrade-0.8.7-0.8.8.php
      подробности будут ниже, когда будем писать инсталятор для нашего модуля.
    3. Обнаруживаем описание изменяемых полей (аттрибутов) в конфигурации модуля:
      /magento/app/code/core/Mage/Customer/etc/config.xml
      внутри тэгов <fieldsets>
    4. Форма регистрации постит данные по адресу:
      http://localhost/magento/customer/account/createpost/

      Соответсвенно запрос обрабатывается контроллером Mage_Customer_AccountController метод createPostAction (line #234)


    Реализация


    Вариант с изменением найденых фалов не рассматриваем. Создадим свою тему и свой модуль.
    С темой всё просто:
    1. создаем новую папку в /magento/app/design/frontend/default/mytheme
      default — имя интерфейса
      mytheme — имя нашей темы
    2. копируем файлы с сохранением структуры директорий в новую тему
    3. редактируем/создаем шаблоны по своему усмотрению

    Одноименные шаблоны нашей темы перекроют шаблоны стандартной темы. Таким образом мы не вносим изменений в "родные" файлы Magento.

    С модулем немного интересней:
    Создаем новый модуль — CustomerMod. Структура папок у всех модулей одинаковая:
    magento/app/code/local/
    	Examples
    		CustomerMod
    			- Block
    			- etc
    			- Helper
    			- Model
    			- sql
    


    не забываем добавить файл с описанием наших модулей:
    magento/app/etc/modules/Examples_All.xml
    <config>
        <modules>
            <Examples_CustomerMod>
                <active>true</active>
                <codePool>local</codePool>
            </Examples_CustomerMod>
        </modules>
    </config>
    


    Поля на странице регистрации


    Начнем с добавления на страницу регистрации поля выбора группы («General», «Wholesale», «Retailer»). Для вывода информации Magento оперирует блоками и шаблонами. Блок можно назвать рендерером шаблона. Каждая страница состоит из блоков, для каждого блока указан шаблон.
    Мы добавим свой блок со своим шаблоном внутрь блока регистрационной формы. Другими словами, наш блок будет дочерним к блоку формы.

    1. Собственно новый шаблон


    Шаблон выбора группы в отдельном файле:
    /magento/app/design/frontend/default/example_theme/template/customer/form/register/groupselect.phtml

    с нехитрым содержанием:

    <?php $customer_groups = Mage::helper('customer')->getGroups()->toOptionArray(); ?>
    
    <fieldset class="group-select">
    <h4 class="legend"><?php echo $this->__('Customer Group') ?></h4>
    <ul>
    <?php foreach($customer_groups as $cg) { ?>
    	<li class="f-left" style="margin: 2px 4px;" >
    		<input type="radio" name="group_id" id="group_id" 
    			value="<?php echo $cg['value']; ?>" 
    			<?php if ($cg['value']==1) echo 'checked=1' ?>  />
    		<label><?php echo $cg['label']; ?></label>
    	</li>
    <?php } //end foreach?>
    </ul>
    </fieldset>
    


    Обратите внимание имя поля «group_id» должно соответсвовать имени аттрибута.

    2. Корректировка layout


    Взаимное расположение блоков и привязка к шаблонам описываются в layout. Это xml-файлы, которые лежат в папке magento/app/design/ИМЯ_ИНТЕРФЕЙСА/ИМЯ_ТЕМЫ/layout.

    В нашем случае будет:
    /magento/app/design/frontend/default/example_theme/layout/customermod.xml
    <layout version="0.1.0">
    	<customer_account_create>					
    		<reference name="customer_form_register">			
    			<block type="core/template" name="customergroups-select" template="customer/form/register/groupselect.phtml" />
    			<block type="captcha/recaptcha" name="captcha" />			
    		</reference>		
    	</customer_account_create>		
    </layout>
    


    Мы ссылаемся на блок customer_form_register. Описываем внутри него наш дочерний блок:
    type — это класс блока, который в конечном итоге рендерит шаблон. В данном случае «core/template», что значит Mage_Core_Block_Template.
    name — имя может быть любым. Оно нужно, чтобы обращатся к блоку, например в reference.
    template — шаблон блока
    Не забываем указать обновления layout в конфигах модуля, чтобы Magento учитывала изменения указанные в customermod.xml:

    /magento/app/code/local/Examples/CustomerMod/etc/config.xml
    <frontend>
    <layout>
          <updates>
              <customermod>
                  <file>customermod.xml</file>
              </customermod>
          </updates>
     </layout>
    </frontend>
    


    3. Вывод блока родительским блоком.


    Наш блок пока ещё не видно, потому что мы должны «вызвать» дочерний блок в родительском с помощью метода getChildHtml(ИМЯ_БЛОКА).
    Копируем register.phtml в нашу тему из default темы и добавляем необходимый вызов перед секцией Login Information:

    /magento/app/design/frontend/default/example_theme/template/customer/form/register.phtml
    ...
    <?php echo $this->getChildHtml('customergroups-select') ?>
       
     <fieldset class="group-select wide">
            <h4 class="legend"><?php echo $this->__('Login Information') ?></h4>
    ...
    


    4. Сохранение данных


    Теперь в регистрационной форме должен появится выбор группы.
    «Но постойте, покупатель регистрируется, указывая „Retailer“ — а в панеле админа он всё равно в General группе!» — да, поле group_id не сохраняется.
    Чтобы введенные поля сохранялись нужно указать их в fieldsets в конфигурации:

    /magento/app/code/local/Examples/CustomerMod/etc/config.xml
        <fieldsets>
                <customer_account>
                    <group_id><create>1</create><update>1</update></group_id>	                
                </customer_account>
            </fieldsets>
    


    Почему — спросите разработчиков Magento, — но так устроен Customer контроллер. Кому интересно — смотрите /magento/app/code/core/Mage/Customer/controllers/AccountController.php метод createPostAction.

    Новый аттрибут


    Аттрибуты описаны в таблице eav_attributes, соответсвенно работа с атрибутами сводится к редактировнию записей таблицы eav_attributes.
    Cуществуют также static аттрибуты, значения которых хранятся в отдельных колонках (например sales_order).

    1. Инсталляционные скрипты


    Все действия по модификации модели производятся в инсталляционных скриптах, хранящихся в папке sql внутри каждого модуля.
    /magento/app/code/core/Mage/Customer/sql/customer_setup
    Скрипты бывают двух видов — setup и upgrade. Magento прогоняет соответсвующий скрипт один раз при установке или обновлении модуля. После установки в таблице core_resource появляется запись, например:
    'customermod_setup', '0.1.0'

    Иными словами скрипты прогоняются по двум причинам:
    1. отсутсвие записи о модуле в таблице core_resource
    2. версия модуля указанная в core_resource ниже, чем в config.xml модуля


    Стоит отметить, что механизма деинсталляции не предусмотрено. Созданные атрибуты удалять придётся руками в случае чего.

    Скрипты исполняются инсталлятором. По-умолчанию класс инсталлятора Mage_Core_Model_Resource_Setup, но он не содержит методов работы с EAV моделью (а мы аттрибуты собираемся создавать), поэтому в нашем случае нужен Mage_Eav_Model_Entity_Setup.

    setup class hierarchy

    2. Скрипт создания атрибута


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

    $installer = $this;
    
    /* @var $installer Mage_Customer_Model_Entity_Setup */
    $installer->startSetup();
    
    $installer->addAttribute('customer', 'invitation_code', array(
    	'type' => 'varchar',
    	'input' => 'text',
    	'label' => 'Invitation Code',
    	'global' => 1,
    	'visible' => 1,
    	'required' => 1,
    	'user_defined' => 1,
    	'default' => null,
    	'visible_on_front' => 1
    ));
    
    $installer->endSetup();
    

    Думаю после стольких предисловий комментрии к самому скрипту не требуются.

    3. Обновляем шаблон


    В шаблон формы регистрации добавляем новое поле:

    /magento/app/design/frontend/default/example_theme/template/customer/form/register.phtml
    <div class="input-box">
    	<label for="invitation_code"><?php echo $this->__('Invitation Code') ?> <span class="required">*</span></label><br/>
    	<input type="text" name="invitation_code" id="invitation_code" title="<?php echo $this->__('Invitation Code') ?>" class="required-entry input-text" />
    </div>
    


    4. Обновляем конфигурацию


    Добавляем описание установочного скрипта:
    /magento/app/code/local/Examples/CustomerMod/etc/config.xml
    <global>
    	<resources>
    		<customermod_setup>
    			<setup>
    				<module>Examples_CustomerMod</module>
    				<class>Mage_Eav_Model_Entity_Setup</class>
    			</setup>
    			<connection><use>core_setup</use></connection>
    		</customermod_setup>
    		<customermod_write><connection><use>core_write</use></connection></customermod_write>
    		<customermod_read><connection><use>core_read</use></connection></customermod_read>
    	</resources>
    

    И не забываем указать новое поле в fieldsets
    <fieldsets>
    	<customer_account>
    		<group_id><create>1</create><update>1</update></group_id>
    		<invitation_code><create>1</create><update>1</update></invitation_code>				                
    	</customer_account>
    </fieldsets>
    

    Теперь в форме регистрации есть поле Invitation Code, значение которого сохраняется в атрибуте invitation_code для каждого клиента. Также новое поле появилось в панели администратора на странице аккаунта клиента в закладке Account Information.

    Admin Customer Account Screenshot

    CAPTCHA



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

    На мой взгляд лучше оформить в отдельном модуле. Шаги по созданию нового модуля, отдельного шаблона, модификации разметки (layout) такие же, что были описаны выше. Остановимся на том, что ещё не было затронуто: контроллер и хелпер.
    Значения с формы регистрации передаются в контроллер Mage_Customer_AccountController В Magento предусмотрен механизм переопределения контроллеров (начиная с версии 1.3 механизм несколько изменился). Вместо изменений в стандартном контроллере, создадим новый, унаследованный от стандартный.

    Итак по-порядку.

    Скачиваем библиотеку recaptcha-php и копируем в папку /magento/lib.

    1. Контроллер



    В конфигурации модуля CustomerMod описываем контроллер:
    <config>
        ...
        <frontend>
        ...
         <routers>
             <customer>
                <args>
                   <modules>
                      <Examples_CustomerMod before="Mage_Customer">Examples_CustomerMod</Examples_CustomerMod>
                   </modules>
                </args>
             </customer>
          </routers>
        ...
    


    Собственно сам контроллер.
    /magento/app/code/local/Examples/CustomerMod/controllers/AccountController.php
    require_once("Mage/Customer/controllers/AccountController.php");
    require_once('recaptcha/recaptchalib.php');
    
    class Examples_CustomerMod_AccountController extends Mage_Customer_AccountController {
    		
    	public function createPostAction() {		
    		$request = $this->getRequest();		
    		$captchaIsValid =  Mage::helper('captcha')->captchaIsValid($request);
    		
    		if ($captchaIsValid) {
    			parent::createPostAction();
    		} else {
    			$this->_getSession()->setCustomerFormData($this->getRequest()->getPost());
    			$this->_getSession()->addError($this->__('Verification code was not correct. Please try again.'));
    			$this->_redirectError(Mage::getUrl('*/*/create', array('_secure'=>true)));
    		}
    	}
    }
    


    В отличие от всех других классов, определяемых в Magento, для контроллеров необходимо явно указывать на файл содержащий родительский класс и сторонние библиотеки, поэтому и нужны два require_once. Код минимален, используем стандартную функцию из recaptchalib. Но сама проверка введенной каптчи вынесена в отдельный класс-хелпер. Если понадобиться добавить такую же проверку в другие контроллеры, то всё сведется к проверке результата Mage::helper('captcha')->captchaIsValid($request).
    Здесь ещё можно добавить, например, подлинность кода приглашения.

    2. Хелпер


    Хелпер в Magento — это класс синглетон, содержащий как правило набор вспомогательных методов. Обращение к хелперу производится с помощью метода Mage::helper() с именем модуля в качестве параметра. В нашем случае Examples_Captcha_Helper_Data будет содержать функции проверки каптчи.

    /magento/app/code/local/Examples/Captcha/Helper/Data.php
    require_once('recaptcha/recaptchalib.php');
    class Examples_Captcha_Helper_Data extends Mage_Core_Helper_Abstract
    {	
    	const CAPTCHA_PUBLIC_KEY = "public-key-for-the-website";
    	const CAPTCHA_PRIVATE_KEY = "private-key-for-the-website";
    	
    	public function captchaIsValid(Mage_Core_Controller_Request_Http $request) {	
    		if ($request) {
    			$resp = recaptcha_check_answer (self::CAPTCHA_PRIVATE_KEY,
    		                           $_SERVER["REMOTE_ADDR"],
    		                           $request->getParam("recaptcha_challenge_field"),
    		                           $request->getParam("recaptcha_response_field") );		
    			return $resp->is_valid;
    		} 
    		return false;						
    	}
    	
    	public function captchaGetError(Mage_Core_Controller_Request_Http $request) {
    		if ($request) {
    			$resp = recaptcha_check_answer (self::CAPTCHA_PRIVATE_KEY,
    			                           $_SERVER["REMOTE_ADDR"],
    			                           $request->getParam("recaptcha_challenge_field"),
    			                           $request->getParam("recaptcha_response_field") );
    			return $resp->error;
    		}
    		return false;
    		
    	}
    	
    	public function getPublicKey() { return  Examples_Captcha_Helper_Data::CAPTCHA_PUBLIC_KEY; }
    	
    }
    


    3. Блок CAPTCHA


    Не плохо бы вывести картинку самой каптчи на страницу. Есть для этого функция recaptcha_get_html(). Не смотря на то, что функцию можно вызвать из шаблона (phtml), мы последуем идеям и архитектуре Magento — cоздадим новый тип блока, заодно узнаем как блок может быть и без шаблона. Для этого опишем класс Examples_Captcha_Block_Recaptcha. Вызов функции recaptcha_get_html() внесем в метод _toHtml. Этот метод вызывается при отрисовке блока в HTML. (см. /magento/app/code/core/Mage/Core/Block/Abstract.php line #643)

    /magento/app/code/local/Examples/Captcha/Block/Recaptcha.php
    require_once('recaptcha/recaptchalib.php');
    class Examples_Captcha_Block_Recaptcha extends Mage_Core_Block_Abstract {		
    	
    	public function _toHtml() {
    		$html = recaptcha_get_html( Mage::helper('captcha')->getPublicKey() );
    		return $html;
    	}
    	
    }
    


    Добавляем новый блок в layout. Шаблон для этого блока не нужен, он и так выводит готовую каптчу.

    /magento/app/design/frontend/default/example_theme/layout/customermod.xml
    <?xml version="1.0"?>
    <layout version="0.1.0">
    <customer_account_create>					
    		<reference name="customer_form_register">			
    			<block type="core/template" name="customergroups-select" template="customer/form/register/groupselect.phtml" />
    			<block type="captcha/recaptcha" name="captcha" />			
    		</reference>		
    	</customer_account_create>
    </layout>
    


    4. Конфигурация


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

    /magento/app/code/local/Examples/Captcha/etc/config.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <config>	
    	<modules>
    	   <Examples_Captcha>
    	       <version>0.1.0</version>
    	   </Examples_Captcha>
    	</modules>
    	
    	<global>        		
    		<blocks>
    			<captcha><class>Examples_Captcha_Block</class></captcha>
    		</blocks>				
    		<helpers>
    			<captcha>
    				<class>Examples_Captcha_Helper</class>
    			</captcha>
    		</helpers>				
    	</global>	
    </config>
    


    И каптча готова.

    CAPTCHA screenshot

    Заключение



    Спасибо дочитавшим до конца! Надеюсь вам было интересно или хотя бы полезно :). Можете скачать готовый пример. Для написания статьи использовалась Magento версии 1.3.2.4.
    Если затронутая тема интересна на хабре, то я с радостью выслушаю пожелания по новой статье.
    Есть идеи и кое-какие материалы для статей на тему:
    • Обзор Magento пряники и грабли
    • События и слушатели в Magento на примере добавления email-уведомления.
    • Debugging в Magento (про тестовую страницу, про Firebug, про Mage::log)
    • Модели в Magento. Добавляем свои сущности и атрибуты
    • Как я добавлял блоки в email-шаблоны Magento
    • PDF в Magento. Горькая правда
    • Работа с коллекциями в Magento на примере создания отчета
    • выбор/настройка IDE для разработки в Magento

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 19

      +3
      Рад, что вы получили инвайт, т.к. статья мне очень понравилась. Мне бы было интересно почитать о «Обзор Magento пряники и грабли» и «Модели в Magento. Добавляем свои сущности и атрибуты».

      Сам сейчас делаю наброски о Dataflow, но из-за загруженности работой (и немножко ленью), написание статьи медленно продвигается.
        0
        Где вы были полгода назад?! :)
          +2
          читал форумы Magento :)
          +1
          Было бы интересно почитать про Модели в «Magento. Добавляем свои сущности и атрибуты»
            0
            Отличная статья, но сам бы не решился проделывать то же самое, слишком уж непроста magento да и не программист я. Хотя у нас рабочий проект на ней, чему безмерно рады. Вы пишете модули на заказ / другие доработки делаете?
              0
              Спасибо. Я работаю над магазином, занимаюсь разработкой, сопровождением и т.п. — всё в рамках проекта нашей компании. А про работу «на заказ» подумываю, но пока на это времени нет (если вы об этом). Вообще спецов по Magento становится много, и если вам нужна помощь, то на официальном сайте можно дать объявление.
                0
                Рабочий проект — это интересно. Если есть вопросы, можете писать в личную почту, постараюсь ответить. Потом можно будет с народом поделится. Я за обмен опытом, ведь ради этого и есть Хабр (?)
                0
                Спасибо за пост. Как раз изучаю magento. На русском осваивать вдвойне приятнее.
                  +2
                  Учитывая высокий порог входа Magento, статья неплохая.

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

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

                  <config>
                    ...
                    <default>
                      <recaptcha>
                        <keys>
                          <public_key>public-key-here</public_key>
                          <private_key>private-key-here</private_key>
                        </keys>
                      </recaptcha>
                    </default>
                    ...
                  </config>


                  Получать значения параметров можно таким образом:
                  Mage::getStoreConfig('recaptcha/keys/public_key')


                  Но эти самые ключи еще нужно задавать. Править вручную какие-то ни было файлы конфигурации противоречит идеологии движка, потому будем использовать админ-панель. Для описания формы конфигурации используется файл etc/system.xml в папке написанного модуля:

                  <config>
                    <sections>
                      <recaptcha>
                        <label>ReCapthcha</label>
                        <tab>service</tab>
                        <frontend_type>text</frontend_type>
                        <sort_order>150</sort_order>
                        <show_in_default>1</show_in_default>
                        <show_in_website>1</show_in_website>
                        <show_in_store>1</show_in_store>
                        <groups>
                           <keys>
                            <label>Keys</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>1</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <fields>
                              <public_key>
                                <label>Public key</label>
                                <sort_order>1</sort_order>
                                <show_in_default>1</show_in_default>
                                <show_in_website>1</show_in_website>
                                <show_in_store>1</show_in_store>
                              </public_key>
                              <private_key>
                                <label>Private key</label>
                                <sort_order>2</sort_order>
                                <show_in_default>1</show_in_default>
                                <show_in_website>1</show_in_website>
                                <show_in_store>1</show_in_store>
                              </private_key>
                            </fields>
                          </keys>
                        </groups>
                      </recaptcha>
                    </sections>
                  </config>


                  Этим кодом мы описываем секцию в конфигурации системы, в которой и будет форма для правки наших параметров.
                  Сама система конфигурации Magento достойна нескольких статей, потому в этом примере многое заведомо опущено.
                    0
                    Я с вами полностью согласен и поступаю точно так же, вынося настройки в админку. Но в одной статье обо всём не рассказать, и я решил этот момент убрать (и некоторые другие тоже). Статья и без того получилась довольно большая на мой взгляд, что плохо влияет на восприятие читателем.
                    Примеры упрощены до предельного минимума, чтобы можно было создать работающий модуль и далее совершенствовать его по своему усмотрению.
                    Спасибо вам за приведенный пример. По комментариям к статье мне будет легче выбрать о чем написать в следующий раз.
                    0
                    Очень познавательно, спасибо.
                    Давно интересовался этим движком, но всё никак руки не доходили взяться за него.
                    Крайне была бы любопытна статья на тему «События и слушатели в Magento».
                      0
                      Напишите, пожалуйста, про «Обзор Magento пряники и грабли»
                        0
                        Да, актуально. Сейчас как раз надо новый блок на все страницы добавить.
                          0
                          Тёпнуца сколько проблем для небольшого расширения функционала…

                          Напишите плиз (или дайте ссылку на...) человеческий рассказ про процесс импорта товаров из (в общем-то любого) источника данных? Язык не имеет значения…
                            0
                            Вот Wiki-cтраница про импорт, ну и поиск вам в помощь
                            А вообще, импорт продуктов — это одна из граблей, про которые я собираюсь рассказать в обзоре.
                            0
                            require_once('recaptcha/recaptchalib.php');
                            от такого надо избавляться… при включенной Compile опции поломается :)

                            Выносите в lib в соответствии с конвенцией имен Zend :) Или может вы это уже знали?)

                            ps требуется помощь в layouts Magento
                              0
                              ох, давно это было!.. компания уже успела разориться, с работы выпинули, и с PHP с тех пор не работал :) Собственно поэтому и не появилось второй статьи про Magento. Спасибо за совет, не помню, скорее всего не знал тогда про Compile опции. А про layouts может чего и вспомню, пишите в личную почту, чем смогу помогу.
                              0
                              Е если в форму обзоров надо добавить каптчу, это куда писать?
                                0
                                Чтобы найти как выводится форма, то нужно смотреть конфигурацию Layout, какой блок за это отвечает. Далее смотрите какой контроллер обрабатывает форму обзоров, наследуйте свой контроллер от него и добавляйте новую логику. Как-то так… подробности уже не помню.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое