Hook_node_access_records()
¶
Данный хук записывает в базу информацию о правах доступа к ноде. В нём, мы будем определять, какой уровень доступа должен быть у пользователя, чтобы получить доступ к данному содержимому. В нашем случае, к его просмотру.
Hook_node_grants()
¶
Данный хук отвечает за выдачу прав пользователю. При просмотре ноды, пользователь в зависимости от тех условий что мы задаем, получит определенный уровень доступа к содержимому. Т.е. в данном хуке мы определим уровни доступа.
В чём плюсы
- В отличии от блокировки доступа средствами Rules, текущий вариант железный. Нету роли – нету доступа.
- Можно гнуть как угодно. Любые условия, проверки и т.д. На что хватит фантазии.
- В результатах Views не будут отображаться ноды, доступа к которым не имеет пользователь. Их увидят только те, кто подходит под условия.
Всё не так уж и сложно. Главное уловить идею про выдачу уровней доступа и понимание придёт. Удачи в создании закрытых разделов 😉
Глазами open-id
Казалось бы давно решенная проблема. Есть же авторизация/регистрация по OpenID. В drupal встроена в ядро. Но пользователь, который только что вылез из ВКонтакте, скорее всего даже слов таких не слышал и пройдет мимо данной возможности.
Глазами программиста
Программистов тоже можно понять. Во-первых, спамеры, во-вторых, сеошники. Лезут во все дыры сайта, суют свои поганые ссылки на «энладж ё пенис» и «чиап лоан». Вот и вырастают заборы из кептч, проверок, анкет….
И на эти заборы кроме спамеров натыкаются и те, кто, возможно, собирается что то купить на сайте, а терпения на форму регистрации у них уже может не хватить.
Модуль в drupal
Вы можете создать новый модуль для друпал или использовать существующий для размещения следующих hook-ов и объявлений. Пусть модуль называется «loginza» для определенности.
Создаём раздел сайта — вход для обработки данных авторизации
далее сам обработчик.
} else {
//авторизация с использованием email
//(часть кода заимствована из ядра drupal)
if (!empty($result->email) && valid_email_address($result->email)) {
//проверим e-mail по базе
$res = db_query(“SELECT * FROM users WHERE mail like ‘%s'”, $result->email);
if (mysql_numrows($res)) {
//есть такой юзер – авторизация под ним
$account = user_load(array(‘mail’ => $result->email, ‘status’ => 1));
if ($account) {
if (drupal_is_denied(‘mail’, $account->mail)) {
drupal_set_message(t(‘The name %name is registered using a reserved e-mail address and therefore could not be logged in.’, array(‘%name’ => $account->name)));
} else {
$user = $account;
user_authenticate_finalize($form_values);
//идем на “главную” после авторизации
drupal_goto(‘<front>’);
}
} else {
drupal_set_message(‘Учетная запись данного пользователя заблокирована!’);
return ‘<p>Так как учетная запись заблокирована, то авторизация
с её использованием невозможна.</p>’;
}
}
}
//авторизация с использованием внешнего идентификатора
//проверим identity по базе
$idn = substr($result->identity, 7); // отрежем http://
$res = db_query(“SELECT * FROM users WHERE `identity` like ‘%s'”, $idn);
if (mysql_numrows($res)) {
//есть такой юзер – авторизация под ним
$account = user_load(array(‘identity’ => $idn, ‘status’ => 1));
if ($account) {
if (drupal_is_denied(‘mail’, $account->mail)) {
drupal_set_message(t(‘The name %name is registered using a reserved e-mail address and therefore could not be logged in.’, array(‘%name’ => $account->name)));
} else {
$user = $account;
user_authenticate_finalize($form_values);
//идем на “главную” после авторизации
drupal_goto(‘<front>’);
}
} else {
drupal_set_message(‘Учетная запись данного пользователя заблокирована!’);
return ‘<p>Так как учетная запись заблокирована,
то авторизация с её использованием невозможна.</p>’;
}
} else {
//в прочих случаях проводим регистрацию пользователя
$userinfo = array(
‘identity’ => $idn,
‘mail’ => $result->email,
‘name’ => $result->name->first_name . ‘ ‘ . $result->name->last_name,
/* генерируем пароль, пользователь его вряд ли будет менять когда-либо,
потому лучше сделать его качественным */
‘pass’ => myLIB::pwd_generator(),
‘init’ => $result->name->first_name . ‘ ‘ . $result->name->last_name,
‘status’ => 1,
‘access’ => time()
);
$account = user_save(”, $userinfo);
// Terminate if an error occured during user_save().
if (!$account) {
drupal_set_message(t(“Error saving user account.”), ‘error’);
return ‘<p>Не удалось создать уникальной учетной записи по данным ‘
. $result->provider . ‘!</p>’;
}
$user = $account;
//идем на “главную”
drupal_goto(‘<front>’);
}
}
} else {
return ‘
<script src=”https://loginza.ru/js/widget.js” type=”text/javascript”></script>
<iframe src=”https://loginza.ru/api/widget?overlay=loginza&token_url=http://’ . $_SERVER[‘HTTP_HOST’] . ‘/user_profile”
style=”width:359px;height:300px;” scrolling=”no” frameborder=”no”></iframe>’;
}
}
}
/* обработка данных авторизации через логинзу-ру */ function_loginza_page(){ global$user; if($user–>uid){ //уже авторизирован drupal_goto(‘user/’.$user–>uid); }else{ if(!empty($_POST[‘token’])){ //два ключа – варианты с www и без него switch(strtolower($_SERVER[‘HTTP_HOST’])){ case‘ваш-сайт.домен’: $skey=‘…’;//секретный ключ $id=‘..’;//ID break; case‘www.ваш-сайт.домен’: $skey=‘…’;//секретный ключ для сайта с www $id=‘..’;//ID для сайта с www } //проверка, что данные пришли с логинзы $sig=md5($_POST[‘token’].$skey); $responde=file_get_contents(“http://loginza.ru/api/authinfo” .“?token={$_POST[‘token’]}&id=$id&sig=$sig”); $result=json_decode($responde); // print_r($result); – для тестовых ситуации можно “раскоментить” 🙂 if(!empty($result–>error_type)){ //не удачно прошла авторизация – как раз тестовая ситуация 🙂 return‘<p>Авторизация не была завершена или произошла ошибка!</p>’; }else{ //авторизация с использованием email //(часть кода заимствована из ядра drupal) if(!empty($result–>email)&& valid_email_address($result->email)) { //проверим e-mail по базе $res = db_query(“SELECT * FROM users WHERE mail like ‘%s'”, $result->email); if(mysql_numrows($res)){ //есть такой юзер – авторизация под ним $account=user_load(array(‘mail’=>$result–>email,‘status’=>1)); if($account){ if(drupal_is_denied(‘mail’,$account–>mail)){ drupal_set_message(t(‘The name %name is registered using a reserved e-mail address and therefore could not be logged in.’,array(‘%name’=>$account–>name))); }else{ $user=$account; user_authenticate_finalize($form_values); //идем на “главную” после авторизации drupal_goto(‘<front>’); } }else{ drupal_set_message(‘Учетная запись данного пользователя заблокирована!’); return‘<p>Так как учетная запись заблокирована, то авторизация с её использованием невозможна.</p>’; } } } //авторизация с использованием внешнего идентификатора //проверим identity по базе $idn=substr($result–>identity,7);// отрежем http:// $res=db_query(“SELECT * FROM users WHERE `identity` like ‘%s'”,$idn); if(mysql_numrows($res)){ //есть такой юзер – авторизация под ним $account=user_load(array(‘identity’=>$idn,‘status’=>1)); if($account){ if(drupal_is_denied(‘mail’,$account–>mail)){ drupal_set_message(t(‘The name %name is registered using a reserved e-mail address and therefore could not be logged in.’,array(‘%name’=>$account–>name))); }else{ $user=$account; user_authenticate_finalize($form_values); //идем на “главную” после авторизации drupal_goto(‘<front>’); } }else{ drupal_set_message(‘Учетная запись данного пользователя заблокирована!’); return‘<p>Так как учетная запись заблокирована, то авторизация с её использованием невозможна.</p>’; } }else{ //в прочих случаях проводим регистрацию пользователя $userinfo=array( ‘identity’=>$idn, ‘mail’=>$result–>email, ‘name’=>$result–>name–>first_name.‘ ‘.$result–>name–>last_name, /* генерируем пароль, пользователь его вряд ли будет менять когда-либо, потому лучше сделать его качественным */ ‘pass’=>myLIB::pwd_generator(), ‘init’=>$result–>name–>first_name.‘ ‘.$result–>name–>last_name, ‘status’=>1, ‘access’=>time() ); $account=user_save(”,$userinfo); // Terminate if an error occured during user_save(). if(!$account){ drupal_set_message(t(“Error saving user account.”),‘error’); return‘<p>Не удалось создать уникальной учетной записи по данным ‘ .$result–>provider.‘!</p>’; } $user=$account; //идем на “главную” drupal_goto(‘<front>’); } } }else{ return‘ <script src=“http://loginza.ru/js/widget.js”type=“text/javascript”></script> <iframe src=”https://loginza.ru/api/widget?overlay=loginza&token_url=http://’.$_SERVER[‘HTTP_HOST’].‘/user_profile” style=”width:359px;height:300px;” scrolling=”no” frameborder=”no”></iframe>’; } } } |
Небольшой кусочек сервисной библиотеки. Тут только ф-ция генерации пароля.
Нюансы
¶
Имейте ввиду, что главный админ сайта будет обходить все эти проверки. Так что дебажить эти хуки не получится при помощи devel. Либо создайте пользователя с доступом к девелу, либо включите специальный для этого модуль Devel node access и вынесите соответствующий блок в видный регион. Он будет красиво, в табличке показывать, кто что может, а кто нет.
Подготовка сайта
¶
Это по сути пошаговый туториал для начинающих. Если вы хорошо разбираетесь в коде, то и воссоздавать среду для тестов не придётся.
Я не особо хочу заморачиваться с подготовкой сайта, поэтому всё очень просто:
- Устанавливаем Drupal с профилем Standart.
- Типу содержимого Article добавляем поле: 1. Название: “Для своих”;
- Машинное имя: field_members_only;
- Тип: boolean;
- Виджет: Single onoff checkbox.
- Добавляем новую роль для пользователей: Members и располагаем после авторизованных пользователей.
Пояснения
Чтобы ещё больше внести ясности, вот небольшое пошаговое объяснение.
Практическая часть
¶
Собственно код со всеми комментариями.
/**
* @file
* Здесь мы будем писать весь код.
*/
/**
* Для начала объявим константы для более удобного контроля доступа.
* Так как он весь завязан на цифрах от 0 и выше, то чтобы не запутаться, проще
* вынести их в константы. Так будет намного читабельнее и яснее что делается.
*
* MYMODULE_ACCESS_REALM - название нашего "реалма" внутри которого будут
* выдаваться права. Это что-то вроде машинного имени для наших уровней доступа
* внутри которого отрабатывают наши условия.
*
* А также объявляем две константы с уровнем доступа:
* - MYMODULE_ACCESS_PUBLIC: которая равняется нулю, что в свою очередь
* является уровнем доступа для просмотра публичных материалов.
* - MYMODULE_ACCESS_PRIVATE: равняется единице, что будет соответстовать
* праву на просмотр скрытого содержимого.
*
* Цифры могут быть любыми, задаются на усмотрение. Но для понимания мы делаем
* их в порядке увеличения. Чем выше цифра - тем больше прав.
*/
define('MYMODULE_ACCESS_REALM', 'mymodule_access_article');
define('MYMODULE_ACCESS_PUBLIC', 0);
define('MYMODULE_ACCESS_PRIVATE', 1);
/**
* Используем hook_node_grants().
*
* Данный хук срабатывает при просмотре содержимого и выдаёт пользователю
* соответствующий уровень доступа к содержимому.
*
* $account - информация о пользователе, который обратился к ноде.
* $op - операция которая выполняется (view, edit, delete).
*/
function mymodule_node_grants($account, $op) {
// Нас интересует лишь просмотр содержимого. Поэтому права мы выдаем именно
// в момент просмотра содержимого. Редактирование и удаление будет ограничено
// системными правами (что в админке друпала).
if ($op == 'view') {
// Теперь мы проверяем, имеет ли текущий пользователь роль 'Members'.
// Т.е. условие может быть каким угодно, но в нашем случае, мы определяем
// будет ли иметь доступ по роли.
if (in_array('Members', $account->roles)) {
// Наш пользователь имеет роль 'Members' и мы выдаем ему права на
// просмотр публичного И приватного содержимого.
// Если указать только права на приватное содержимое, то пользователь
// не сможет увидеть публичное.
$grants[MYMODULE_ACCESS_REALM] = array(
MYMODULE_ACCESS_PUBLIC,
MYMODULE_ACCESS_PRIVATE,
);
}
else {
// Ну а если у пользователя нету роли 'Members' то мы разрешаем смотреть
// только публичные материалы.
$grants[MYMODULE_ACCESS_REALM] = array(
MYMODULE_ACCESS_PUBLIC,
);
}
return $grants;
}
}
/**
* Используем hook_node_access_records().
*
* В данном хуке определяется, какой уровень доступа необходим для ноды.
* Данная запись делется при редактированиидобавлении нового материала.
*
* Если у вас уже есть содержимое, которому нужно "пересобрать" права, то
* воспользуйтесь фукнцией node_access_rebuild() или в админке:
* admin/reports/status/rebuild
*/
function mymodule_node_access_records($node) {
// Мы задаем права доступа только для нашего типа содержимого 'Article'.
if ($node->type == 'article') {
// Получаем значения поля "Для своих".
$members_only = field_get_items('node', $node, 'field_members_only');
// Если отмечено "Для своих".
if ($members_only[0]['value']) {
// Указываем ноде, что смотреть её могут пользователи только с gid
// который отвечает за просмотр приватного содержимого.
// Также обратите внимание что у обновления и удаления у нас стоят нули
// так как мы выдаём лишь на просмотр.
$grants[] = array(
'realm' => MYMODULE_ACCESS_REALM,
'gid' => MYMODULE_ACCESS_PRIVATE,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
);
}
else {
// Если не отмечено "Для своих", то мы открываем материал всем желающим.
$grants[] = array(
'realm' => MYMODULE_ACCESS_REALM,
'gid' => MYMODULE_ACCESS_PUBLIC,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
);
}
}
return $grants;
}
Теоретическая часть
¶
Все крутится вокруг двух хуков hook_node_grants() и hook_node_access_records().
Узкие места
Еще раз опишу узкие места. Они не являются неустранимыми, обращаю на них ваше внимание.
Не уникальность имени. В данном варианте код не может зарегистрировать двух пользователей с одинаковым именем. Это не удобно. Будет выдано соответствующее сообщение.Решение: Можно придумать какую то схему уникализации.
Авторизация по существующему в базе email. Программа доверяет данным из СС, и считает, что e-mail проверен на принадлежность пользователю. Это не всегда может быть так. Тогда злоумышленник, зная email админа, к примеру, регистрируется в какой то CC.
Добивается там нужного значения email — и дело сделано, мы имеем взлом аккаунта.Решение: Тут простой путь только один — проводить проверку e-mail, т.е. высылать проверочный код, а потом уже актуализировать авторизацию. Эх, а как не хочется напрягать юзеров лишний раз что то делать!
Шаг 1. подготовка
Этот этап нет смысла описывать подробно, поэтому вкратце:
Добавим две дополнительные роли на сайте:
- Физическое лицо
- Юридическое лицо
С помощью модуля Auto Assign Role разрешим пользователям выбирать свою роль при регистрации. На форме регистрации должы появится соответствующие радиокнопки. Установим и настроим модуль Ajax Login/Register. Установим и настроим модуль Legal.
Шаг 2. магия
Для начала заварим кружку горячего кофе и зарегистрируем адрес для ajax запроса. По этому пути будем обрабатывать форму после заполнения всех шагов. Если у Вас не установлен модуль Ajax Login/Register – то можно обойтись и без этого.
/** * Implements hook_menu(). */ function multistep_register_menu() { $items = array(); $items['ajax_multistep_register'] = array( 'page callback' => '_multistep_register_ajax_callback', 'access callback' => TRUE, 'type' => MENU_CALLBACK, 'delivery callback' => 'ajax_deliver', 'theme callback' => 'ajax_base_page_theme', ); return $items; }
Ну а теперь начинаем делать магию. При помощи hook_form_FORM_ID_alter модернизируем форму регистрации.
Шаг 3. ajax-обработчик формы.
Тут даже комментировать ничего не буду.
function multistep_register_ajax_callback($form, &$form_state) { return $form; }
Шаг 4. дополнительный валидатор формы
У формы регистрации есть свой набор функций для проверки введенных значений.
Есть несколько вариантов, как реализовать валидацию формы в данном случае:
- Удалить стандартные валидаторы и написать один свой, который будет проверять значения в зависимости от текущего шага;
- Написать отдельные валидаторы для каждого шага и вешать их в hook_form_FORM_ID_alter;
- Написать валидаторы для полей и подцеплять их к полям;
- Использовать стандартные валидаторы формы добавить дополнительную проверку по шагам, если вдруг стандартных Вам не хватает.
Основная проблема заключается в том, что стандартные валидаторы работают со всей формой сразу, а у нас после каждого шага заполнен только определенный набор значений, остальные поля – пустые.
Что бы решить эту проблему, будем перед проверкой дополнять массив $form_state[‘values’] значениями с предыдущих шагов. Конечно, для нашей формы поля Имя пользователя и E-mail обязательно должны быть на первом шаге, иначе, придется отключать стандартные проверки для начальных шагов.
function multistep_register_myvalidate($form, &$form_state) { $current_step = empty($form_state['storage']['step']) ? 1 : $form_state['storage']['step']; if ($current_step>1) { for($i=1;$i<$current_step;$i ){ if(isset($form_state['storage']['values']['step'.$i])){ $form_state['values']=array_merge($form_state['values'], $form_state['storage']['values']['step'.$i]); } } } //Теперь добавим свою проверку в зависимости от текущего шага switch ($current_step) { case 1: if (!preg_match("/^[0-9a-zA-Z-_] $/i", $form_state['values']['name'])) { form_set_error('name', 'Недопустимые символы в поле "Имя пользователя"'); } break; } }
Шаг 5. обработчик формы
function multistep_register_form_submit($form, &$form_state) { //Узнаем текущий шаг $current_step = 'step' . $form_state['storage']['step']; // Если перешли на следующий шаг - то увеличиваем счётчик шагов и // сохраняем состояние формы, полученное при переходе на новый шаг. if (isset($form['actions']['next']['#value']) && $form_state['triggering_element']['#value'] == $form['actions']['next']['#value']) { $form_state['storage']['step'] ; // для сохранения текущих значений будет написана отдельная функция $form_state['storage']['values'][$current_step] = _multistep_register_update_values_step($form_state['storage']['fields_step'][$current_step], $form_state['values']); } // Если вернулись на шаг назад - уменьшаем счётчик шагов. // if (isset($form['actions']['prev']['#value']) && $form_state['triggering_element']['#value'] == $form['actions']['prev']['#value']) { $form_state['storage']['step']--; } $form_state['rebuild'] = TRUE; }
Дополнительная функция, которая формирует массив значений полей только для текущего шага.
function _multistep_register_update_values_step($fields, $values) { $items=array(); foreach($fields as $name){ if(isset($values[$name])){ $items[$name]=$values[$name]; } } return $items; }
Шаг 6. добиваем popup
Уже сейчас все должно работать как задумано. Кроме совместимости с модулем Ajax Login/Register. У меня возникало много непонятных проблем, вот самый оптимальный вариант решения – переопределить ajax обработчик для кнопки Отправить. Основа тут – Заставляем любую форму выполняться через AJAX в Drupal 7