Былина о том, как я Drupal и Яндекс.ПДД связывал

в 13:26, , рубрики: drupal, Веб-разработка, модули drupal, Яндекс API, яндекс.почта для домена

Некоторое время назад у меня была идея сделать городской сайт (естественно, очередной) для одного небольшого городка на базе Drupal. Как раз незадолго до этого Яндекс расширил функционал своей Почты для доменов и добавил возможность управлять ящиками через API. И в голове зародилась мысль: а почему бы и нет? Почему бы не предоставить пользователям возможность одновременно с регистрацией на городском портале получать почтовый ящик в городском домене? Сама по себе идея, конечно, не удивительна, однако готовых решений не было. Сайт я так и не запустил, а коду без дела лежать грех.

Как известно, Drupal позиционируется как CMS, ориентированная разработчиков и позволяющая благодаря обширной API разработать хоть систему для управления лунной базой. Я не буду вдаваться в своём посте в её восхваление и самые начальные основы разработки модулей под Drupal, однако начинающие Drupal-разработчики наверняка сочтут материал полезным, а опытным буду благодарен за советы и рекомендации.

Былина о том, как я Drupal и Яндекс.ПДД связывал - 1

Постановка задачи

Естественно, приступая к написанию модуля, нужно решить, каким функционалом он будет обладать. Модуль рассчитан на социальный сайт, где смогут регистрироваться пользователи. Следовательно, он должен:

  • при регистрации пользователя предлагать ему возможность создать ящик на Яндексе;
  • при подтверждении регистрации инициировать создание ящика;
  • отображать авторизированному пользователю блок с информацией о количестве новых писем и возможностью перейти в ящик;
  • создавать админ-часть для ввода необходимых параметров.

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

.info-файл

Любой модуль Drupal должен содержать .info-файл со служебной информацией о модуле. Минимальный набор — название модуля, краткое описание и версия ядра, для которой он написан. Но в нашем случае необходимо указать ещё одно поле — зависимости. В логике модуля заложено создание ящиков, логин которых совпадает с логином пользователя. Но чтобы не лишать пользователей возможность создавать кириллические логины и не удивлять ими Яндекс, нам нужен модуль транслитерации. Таким образом, файлик yandex_pdd.info у нас будет выглядеть следующим образом:

name = Yandex PDD
description = Yandex PDD mailboxes autocreation.
core = 7.x
dependencies[] = transliteration

Естественно, перед установкой модуля нужно не забыть установить у себя этот самый transliteration.

Файл установки и удаления

Ещё один обязательный файл — .install (в нашем случае — yandex_pdd.install), который определяет используемые модулем базы данных и необходимые действия при установке и удалении модуля. Для работы нам понадобится одна база данных и две переменных. Для начала определим структуру базы (хук hook_schema())

function yandex_pdd_schema() {
  $schema['yandex_pdd'] = array(
    'fields' => array(
      'id' => array('type' => 'serial', 'not null' => TRUE), // Идентификатор строки
      'uid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0,), // Идентификатор пользователя
      'login' => array('type' => 'varchar', 'length' => 100, 'not null' => TRUE), // Транслитерированный логин для создания ящика
      'activated' => array('type' => 'varchar', 'length' => 1, 'not null' => TRUE) // Флаг активации ящика
    ),
   'primary key' => array('id'),
  );
  return $schema;
}

Первые две колонки, думаю, вопросов не вызывают. В третью колонку мы будем записывать логин самого электронного ящика. Четвёртую колонку стоит разобрать отдельно. В CMS Drupal для активации пользователя необходимо, чтобы он перешёл по ссылке, присланной ему на e-mail, и задал свой пароль. Известный факт, что часть учётных записей никогда не будет активирована (часть спам-ботов и просто странные люди), потому создавать ящик имеет смысл, когда пользователь задаёт новый пароль. Собственно, это заодно позволяет нам создать ящик с этим же паролем. Данный вопрос я ещё затрону ниже.

При удалении модуля описанные в схеме базы удаляются автоматически. А вот переменные, закешированные данные и т.п. следует за собой убрать с помощью hook_uninstall().

function yandex_pdd_uninstall() {
  variable_del('yandex_pdd_domain');
  variable_del('yandex_pdd_authtoken');
  cache_clear_all('yandex_pdd','cache',TRUE);
  menu_rebuild();
}

Параметры yandex_pdd_domain и yandex_pdd_authtoken задают домен, в котором будут создаваться ящики, и API-ключ соответственно. Расскажу о них подробнее немного позже.

Необходимый фундамент для работы модуля заложен, и мы можем приступать к написанию самого кода модуля. Храниться он у нас будет, как и полагается, в файле с расширением .module (yandex_pdd.module).

Элементы меню

Если модулю Drupal необходимо прописываться в меню, создавать какие-либо страницы и т.п., нам нужно описать всю структуру и необходимые действия при переходе по ссылкам с помощью хука hook_menu(). В нашем случае будет использоваться два URL-а: страница настройки модуля и переадресация в почтовый ящик.

function yandex_pdd_menu(){
	$items = array();
 
	$items['admin/config/content/yandex_pdd'] = array( // Ключ массива - системный путь к странице
		'title' => t('Yandex PDD'), // <title> страницы
		'page callback' => 'main_config', // Функция, которая отвечает за генерацию страницы
		'type' => MENU_NORMAL_ITEM, // Способ подвязки страницы в систему меню CMS
		'access callback' => TRUE, // Определяет возможность доступа к странице
	);
 
	$items['mailbox'] = array(
		'title' => 'Yandex PDD login',
		'page callback' => 'mailbox_login',
		'type' => MENU_CALLBACK, 
		'access callback' => TRUE, 
	);

	return $items;
}

Обратите внимание на использование системной функции t(). Она позволяет переводить элементы текста на текущий системный язык. И да, на странице mailbox нам такая возможность не понадобится.

Страница настройки

В предыдущем разделе мы рассказали нашей CMS, что страница для ввода необходимых модулю данных будет размещаться по пути admin/config/content/yandex_pdd и обрабатываться функцией main_config. Собственно, CMS на самом деле не в курсе, чем конкретно будет заниматься страница, но знает, что при обращении по данному пути нужно обратиться к данной функции.

function main_config(){
	$form = drupal_get_form('pdd_config_form'); // Получаем поля формы
	$form = drupal_render($form); // Рендерим форму
	return $form;
}

Для создания страницы мы получаем друпалопонятный массив полей формы обращением системной функции drupal_get_form() к пользовательской функции pdd_config_form.

function pdd_config_form($form, &$form_state){
	$form=array();
	$form['pdd_domain'] = array( // Ключ массива - имя поля в форме
		'#type'	=> 'textfield', // Тип поля
		'#title' => t('Domain zone'), // Название поля, понятное пользователю
		'#description' => t('A domain zone in which email should be created.'), // Описание поля
		'#default_value' => variable_get('yandex_pdd_domain'), // Значение по умолчанию
		'#required' => 1, // Флаг обязательности
	);
	$form['authtoken'] = array(
		'#type'	=> 'textfield',
		'#title' => t('Auth Token'),
		'#description' => t('Authorization token obtained at Yandex.PDD service.'),
		'#default_value' => variable_get('yandex_pdd_authtoken'),
		'#required' => 1,
	);
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => t('Submit'),
	);
	return $form;
}

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

Форма содержит всего два поля:

  • pdd_domain — подвязанный к Яндекс.ПДД домен, в котором будут создаваться ящики;
  • authtoken — авторизационный токен, полученный в настройках API Почты для домена.

Былина о том, как я Drupal и Яндекс.ПДД связывал - 2

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

function pdd_config_form_submit($form, &$form_state){
	variable_set('yandex_pdd_domain', $form_state['values']['pdd_domain']);
	variable_set('yandex_pdd_authtoken', $form_state['values']['authtoken']);
}

Ещё небольшой кусочек кода — реализация хука hook_help(), который выводит справочную информацию о модуле в соответствующем разделе админ-части. По поводу этой функции я не заморачивался и пошёл по пути минимализма.

function yandex_pdd_help($path, $arg) {
   switch ($path) {
     case "admin/help#yandex_pdd":
       return '<p>'. t("Yandex PDD mailboxes management module.") .'</p>';
       break;
   }
}

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

Создание ящика

Не всем нашим пользователям может понадобиться ещё один e-mail, пусть даже и в домене любимого сайта. Да и нам ни к чему плодить огромное количество неиспользуемых ящиков. Потому мы добавим в форму регистрации пользователя чекбокс, который позволит ему выбирать, создавать ящик или нет. Любую форму в Drupal можно изменить с помощью hook_form_alter(). С назначением некоторых элементов массива описания формы мы уже знакомы. Подробнее со всеми остальными можно познакомиться на справочной странице по формам.

function yandex_pdd_form_alter(&$form, &$form_state, $form_id){
	if($form_id=='user_register_form'){ // Идентификатор изменяемой формы
		$form['account']['createmail']=array( // Имя дополнительного поля
			'#type' => 'checkbox',
			'#title' => t('Create a mailbox'),
			'#description' => t('Check this box if you want to create a mailbox @'.variable_get('yandex_pdd_domain').'.'),
			'#required' => 0,
			'#access' => 1, // Доступно ли поле пользователям
			'#weight' => 10 // Вес (критерий сортировки)
		); 
	}
}

Много текста для одного маленького чекбокса.
Былина о том, как я Drupal и Яндекс.ПДД связывал - 3
При регистрации пользователя мы проверяем, отмечен ли чекбокс создания ящика в форме, и если да, вносим в таблицу данные о неактивном ящике.

function yandex_pdd_user_insert(&$edit, $account, $category){
	if($account->createmail){
		$transliterated = transliteration_get($account->name, '_'); // Транслитерируем логин
		$pattern = '/[^a-zA-Z0-9]/'; // Задаём шаблон для замены всего, кроме букв и цифер 
		$transliterated = preg_replace($pattern, '-', $transliterated); // Заменяем все не alphanumeric знаки на дефисы
		$newbox = db_insert('yandex_pdd'); // Инициируем вставку в базу данных
		$newbox->fields(array('uid' => $account->uid, 'login' => strtolower($transliterated), 'activated' => '0')); // Задаём данные для вставки
		$res = $newbox->execute(); // Выполняем запрос
		watchdog('yandex_pdd',print_r('Res: '.$res,1)); // Для отладки записываем в системный журнал CMS результат 
	}
}

Создание ящика подвязано к изменению формы с пользовательскими данными хуком hook_field_attach_submit(). Таким образом, до того, как пользователь впервые произведёт редактирование данных (изменение пароля), ящик не создаётся.

function yandex_pdd_field_attach_submit($entity_type, $entity, $form, &$form_state) {
	global $user;
	if($entity_type == 'user' and $user->uid > 0) { // Обработка при изменении сущности типа user
		$select = db_select('yandex_pdd','ypdd');
		$select->addField('ypdd', 'id');
		$select->addField('ypdd', 'login');
		$select->addField('ypdd', 'activated');
		$select->condition('uid', $user->uid);
		$entries = $select->execute()->fetchAssoc(); // Получаем логин для ящика и статус активации
		if (array_key_exists('login', $entries) and $entries['login'] != '' and $entries['activated'] == 0) { // Если пользователю нужен ящик и он не создан
			$mailboxcreate = simplexml_load_file('https://pddimp.yandex.ru/reg_user_token.xml?token='.variable_get('yandex_pdd_authtoken').'&u_login='.$entries['login'].'&u_password='.$form["#user"]->pass); // Создаём ящик и парсим XML-ответ
			if ($mailboxcreate->ok[0]) { // Если создался
				$num_updated = db_update('yandex_pdd');
				$num_updated->fields(array('activated' => '1'));
				$num_updated->condition('uid', $user->uid);
				$res = $num_updated->execute(); // Отмечаем флаг активации ящика
			} elseif ($mailboxcreate->error[0]) { // Если ошибка API
				foreach($mailboxcreate->error[0]->attributes() as $key => $value) {
					$mbc[$key] = (string)$value;
				}
				watchdog('yandex_pdd',"Can't create new mailbox. Reason: ".$mbc['reason']); // Записываем в лог сообщение
			} else { // Если вообще неясно что случилось
				watchdog('yandex_pdd','Unknown error while creating mailbox.'); // Так и запишем
			}
		}
	}
}

Наконец-то наш пользователь получил ящик, хотя не особо знает, как туда попасть. Осталось реализовать функционал перехода на почту.

Рабочий функционал

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

function yandex_pdd_block_info() {
  $blocks['mailbox_status'] = array( // Системное имя блока
    'info' => t('Mailbox status'), // Административное название блока
    'cache' => DRUPAL_CACHE_PER_ROLE, // Режим кеширования
  );
  return $blocks;
}

Для блока нам ещё понадобится задать темизацию и файл с шаблоном оформления. Темизация описывается хуком hook_theme(), описывающим системе реализацию оформления элементов модуля.

function yandex_pdd_theme() {
    return array(
        'yandex_pdd_block' => array( // Системное имя шаблона
            'variables' => array( // Используемые шаблоном переменные
                'newmail' => NULL
            ),
            'template' => 'yandex-pdd-block', // Имя файла шаблона
        )
    );
}

И, собственно, сам файл yandex-pdd-block.tpl.php.

<div class="yandexpdd"><?php print t('You have ').'<a href="/mailbox" target="_blank">'.$newmail.t(' new messages').'</a>.'; ?></div>

Для блока нам нужно получить по API одно-единственное значение — количество новых писем, после чего отрендерить сам блок хуком hook_block_view()

function yandex_pdd_block_view($delta = '') {
	global $user;
	if ($user->uid > 0) { // Проверяем, авторизирован ли пользователь
		$select = db_select('yandex_pdd','ypdd');
		$select->addField('ypdd', 'login');
		$select->condition('uid', $user->uid);
		$entries = $select->execute()->fetchAssoc(); // Получаем логин ящика
		$unreadmailxml = simplexml_load_file('https://pddimp.yandex.ru/get_mail_info.xml?token='.variable_get('yandex_pdd_authtoken').'&login='.$entries['login']); // Получаем от Яндекса информацию о ящике
		if ($unreadmailxml->ok[0]) { // Если запрос успешен
			foreach($unreadmailxml->ok[0]->attributes() as $key => $value) { // Парсим ответ
				$unreadmail[$key] = (string)$value;
			}
			$blocks = array();
			$blocks['subject'] = null;
			$blocks['content'] = theme('yandex_pdd_block', array('newmail' => $unreadmail['new_messages']));
			return $blocks; // Возвращаем блок
		} elseif ($unreadmailxml->error[0]) {
			foreach($unreadmailxml->error[0]->attributes() as $key => $value) {
				$unreadmail[$key] = (string)$value;
			}
			watchdog('yandex_pdd',"Can't get new mail info. Reason: ".$unreadmail['reason']);
		} else {
			watchdog('yandex_pdd','Unknown error while loading new mail info');
		}
	}
}

Сгенерированный блок будет выглядеть примерно так.

Былина о том, как я Drupal и Яндекс.ПДД связывал - 4

Завершающий штрих — системная ссылка mailbox, которая будет перебрасывать пользователя на страницу Яндекс.Почты и авторизировать по одноразовому токену. Как мы помним (или уже не помним), мы ранее задали, что эта ссылка должна обрабатываться функцией mailbox_login. Время жизни токена всего 30 секунд, потому после его получения пользователь тут же должен быть переадресован на страницу авторизации.

function mailbox_login(){
	global $user;
	global $base_url;
	if ($user->uid > 0) {
		$select = db_select('yandex_pdd','ypdd');
		$select->addField('ypdd', 'login');
		$select->condition('uid', $user->uid);
		$entries = $select->execute()->fetchAssoc();
		$tokenxml = simplexml_load_file('https://pddimp.yandex.ru/api/user_oauth_token.xml?token='.variable_get('yandex_pdd_authtoken').'&domain='.variable_get('yandex_pdd_domain').'&login='.$entries['login']); // Получаем одноразовый токен авторизации
		if ($tokenxml->xpath('status/success')) {
			$tokenarr = $tokenxml->xpath('domains/domain/email/oauth-token');
			header('Location: http://passport.yandex.ru/passport?mode=oauth&type=trusted-pdd-partner&error_retpath='.urlencode($base_url.'/').'&access_token='.(string)$tokenarr[0]); // Перебрасываем пользователя на Яндекс.Почту
			drupal_exit(); // Прекращаем любые другие действия CMS
		} elseif ($tokenxml->xpath('status/error')) {
			watchdog('yandex_pdd',"Can't get short-term auth token info. Reason: ".(string)$tokenxml->xpath('action/status/error'));
		}
	}
}

Заключение

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

Автор: Vasiliskov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js