你在这里

Chapter 06: Working with Users

用户是使用Drupal的原因,Drupal能帮助用户创建、合作、沟通和塑造一个在线社区。在本章回顾场景并看一下用户怎样授权、登录及内部表现。我们开始一个练习,$user对象是什么、它的结构是怎样的,然后我们演练用户注册、登录、用户授权的过程。最终我们练习Drupal绑定外部授权系统如LDAP和Pubcookie。

$user对象

为了用户登录,Drupal需要用户激活cookie,关掉cookie的用户在Drupal中还可作为匿名用户(anonymous)。
当系统自举处理的会话阶段,Drupal建立了一个全局$user对象,它表示当前用户身份。如果用户没登录(并且没有会话cookie),那么他或她被当做一个匿名用户。这段代码建立一个匿名用户看起来如下(在includes/bootstrap.inc中):

function drupal_anonymous_user($session = '') {
  $user = new stdClass();
  $user->id = 0;
  $user->hostname = ip_address();
  $user->roles = array();
  $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
  $user->session = $session;
  $user->cache = 0;
  return $user;
}

其它情况,如果用户当前是登录的,$user对象则是通过用户ID连接user表、roles和sessions表来建立的两个表的所有字段值都放置到$user对象中。

注意:用户ID是个整数,在用户注册或通过管理员创建用户时分配,ID是user表的主键。

$user对象可以简单通过在index.php中用global $user; print_r($user)来查看。下面是一个登录用户的$user对象的情形:

stdClass Object (
  [uid] => 1
  [name] => admin
  [pass] => $S$CnUvfOYdoxl/Usy.X/Y9/SCmOLLY6Qldrzjf7EOW0fR4LG7rCAmR
  [mail] => joe@example.com
  [theme] =>
  [signature] =>
  [signature_format] => 0
  [created] => 1277957059
  [access] => 1278254230
  [login] => 1277990573
  [status] => 1
  [timezone] =>
  [language] =>
  [picture] => 0
  [init] => joe@example.com
  [data] =>
  [sid] => 8cnG9e0jsCC7I7IYwfWB0rmRozIbaLlk35IQGN5fz9k
  [ssid] =>
  [hostname] => ::1
  [timestamp] => 1278254231
  [cache] => 0
  [session] => batches|a:1:{i:3;b:1;}
  [roles] => Array (
    [2] => authenticated user
    [3] => administrator )
}

下面是$user对象的成分:

------------------------------------------------------------------------------------------------------------
成分                        说明
------------------------------------------------------------------------------------------------------------
user表提供的
------------------------
uid                        用户的ID,user表的主键,在一个Drupal安装中是唯一的
name                       用户的用户名,用户登录时输入的
pass                       用户密码 安全为sha512哈希表
mail                       用户当前email地址
theme                      此字段已弃用,为兼容而保留
signature                  用户签名,用在用户评论时,只有当评论模块激活后才可以浏览
signature format           用户签名格式,如filtered text full text
created                    用户账号建立的时间-unix时间戳
access                     用户最后访问时间-unix时间戳
login                      用户最后成功登陆的时间-unix时间戳
status                     用户被锁定为0,良好为1
timezone                   用户时区与GMT相距的秒数
language                   用户默认语言,为空,除非多语言呗激活
picture                    用户账号图片的路径
init                       用户注册时提供的初始email地址
data                       能被模块存储到这的任何数据(见下节)
--------------------------------------------------------------------------------------------------------------
成分                        说明
--------------------------------------------------------------------------------------------------------------
user_roles表提供的
------------------------
roles                      当前分配给此用户的角色

sessions表提供的
------------------------
sid                        由PHP分配给这个用户会话的会话ID
Ssid                       由PHP分配给这个用户会话的安全会话ID 
hostname                   用户浏览当前页面时从哪个ip地址来的
timestamp                  一个unix时间戳,表现用户浏览器最后就收到一个完整页面的时间
cache                      每个用户缓存的时间戳
session                    在用户的会话期间可以由模块存到这里打任意的、暂短的数据
--------------------------------------------------------------------------------------------------------------

检测用户是否登录

在一个请求之间,检测用户是否登录的标准方式是测试$user->uid是不是0。为此目的,Drupal有个便利的函数叫user_is_logged_in(),这是一个相当的user_is_anonymous()函数:

if (user_is_logged_in()) {
  $output = t('User is logged in.');
else {
  $output = t('User is an anonymous user.');
}

用户钩子介绍

用户钩子的实现给你的模块一个机会来对执行在用户账号上的不同操作做出反应及修改$user对象,这是一些hook_user的变形,每个变形执行不同的动作:

--------------------------------------------------------------------------------------
Hook function                                                目的
--------------------------------------------------------------------------------------
hook_username_alter(&$name, $account)             改变用户显示的用户名
hook_user_cancel($edit, $account, $method)        用户账号取消时的动作
hook_user_cancel_methods_alter(&methods)          修改账号取消时的方法
hook_user_categories()                            检索用户设置或profile信息变化列表
hook_user_delete($account)                        响应用户删除
hook_user_insert(&$edit, $account, $category)     用户账号建立时
hook_user_load($users)                            当从数据库中载入user对象时的动作
hook_user_login(&$edit, $account)                 用户登录
hook_user_logout($account)                        用户登出
hook_user_operations()                            增加一块用户操作
hook_user_role_delete($role)                      通知其它模块用户角色已删除
hook_user_role_insert($role)                      通知其它模块用户角色已增加
hook_user_role_update($role)                      通知其它模块用户角色已更新
hook_user_update(&$edit, $account, $category)     一个用户账号更新时
hook_user_view($account, $viewmode)               用户账号信息显示时
hook_user_view_alter(&$build)                     用户创建时,模块可以修改结果信息
--------------------------------------------------------------------------------------

警告:不要混淆许多用户钩子中的参数$account和全局$user对象,$account参数是当前操作的用户对象,而全局$user对象是指当前登录的用户,二者通常,但不总是一样。

      理解hook_user_view($account, $view_mode)

hook_user_view()通常用来在用户profile页面增加显示信息(例如你在http://example.com/?q=user/1看到的,见图6-1)。

Figure 6-1. The user profile page, with the blog module and the user module implementing
hook_user_view() to add additional information

让我们练习blog模块怎样在页面上用hook_user_view()增加信息:

/**
 * Implements hook_user_view().
 */
function blog_user_view($account) {
  if (user_access('create blog content', $account)) {
    $account->content['summary']['blog'] = array(
      '#type' => 'user_profile_item',
      '#title' => t('Blog'),
      '#markup' => l(t('View recent blog entries'), "blog/$account->uid", array('attributes' = array('title' => t("Read !username's lastest blog entries.", array('!username' => format_username($account)))))),
      '#attributes' => array('class' => array('blog')),
    )
  }
}

view函数存储一些信息到$user->content,用户profile信息组织进categories,每个分类呈现一个关于用户信息的页面,在图6-1,这只有一个分类,叫History。外面的数组应该按分类名称键化,在前面的例子,键名是summary,对应于History分类(十分明确,它应该制造出键名和分类是相同东西的感觉)。里面的一些数组应该有一个唯一的文本键(这里是blog)并且要有#type、#title、#markup、#attributes元素。类型user_user_profile_item指示Drupal主题布局到modules/user/profile-item.tpl.php。比较代码片段和图6-1,你能看见这些元素是怎么渲染的,列印图6-1的$user->content数组的内容,它使页面展示成图6-1那样:

Array
(
    [#pre_render] => Array
        (
            [0] => _field_extra_fields_pre_render
        )
    [#entity_type] => user
    [#bundle] => user
    [#attached] => Array
        (
            [css] => Array
                (
                    [0] => modules/field/theme/field.css
                )
        )
    [summary] => Array
        (
            [blog] => Array
                (
                    [#type] => user_profile_item
                    [#title] => Blog
                    [#markup] => View recent blog entries
                    [#attributes] => Array
                        (
                            [class] => Array
                                (
                                    [0] => blog
                                )
                        )
                )
           [#type] => user_profile_category
           [#attributes] => Array
               (
                   [class] => Array
                       (
                           [0] => user-member
                       )
               )
          [#weight] => 5
          [#title] => History
          [member_for] => Array
              (
                  [#type] => user_profile_item
                  [#title] => Member for
                  [#markup] => 3 days 11 hours
              )
        )
    [user_picture] => Array
        (
            [#markup] =>
            [#weight] => -10
        )
)

你的模块还可以实现hook_user_view()来在$user->content送到主题之前操作$user->content的profile项目。下面的例子简单从用户profile页面删除blog profile项目,假设这个模块叫ide.module

/**
 * Implements hook_user_view().
 */
function hide_user_view($account, $view_mode = 'full') {
  unset($account->content['summary']['blog']);
}

用户注册流程

依照默认,用户在Drupal站点上注册除了一个用户名和一个有效的e-mail地址外不需要更多的信息。通过实现几个用户钩子模块能增加它们自己的字段到用户注册表单,让我们写一个模块legalagree.module,它提供一个快速的方式来使你的站点适应今天的喜好诉讼的社会。
首先,建立目录sites/all/modules/custom/legalagree,增加下面的文件到这个legalagree目录,然后通过Administer->Site building->Modules激活这个模块。

legalagree.info

name = Legal Agreement
description = Display a dubuious legal agreement during user registration.
package = Pro Drupal Development
core = 7.x
files[] = legalagree.module

legalagree.module

<?php
/**
 * @file
 * Support for dubious legal agreement druring user registration.
 */

/**
 * Implements hook_form_alter().
 */
function legalagree_form_alter(&$form, &$form_state, $form_id) {
  // check to see if the form is the user registration or user profile form
  // if not then return and don't do anything
  if (!($form_id == 'user_register_form' || $form_id == 'user_profile_form')) {
    return;
  }

  // add a new validate function to the user form to handle the legal agreement
  $form['#validate'][] = 'legalagree_user_form_validate';

  // add a field set to wrap the legal agreement
  $form['account']['legal_agreement'] = array(
    '#type' => 'fieldset',
    '#title' => t('Legal agreement')
  );

  // add the legal agreement radio buttons
  $form['account']['legal_agreement']['decision'] = array(
    '#type' => 'radios',
    '#description' => t('By registering at %site-name, you agree that at any time, we (or our surly, brutish henchmen) may enter your place of residence and smash your belongings with a ball-peen hammer.', array('%site-name' => variable_get('site_name', 'drupal'))),
    '#default_value' => 0,
    '#options' => array(t('I disagree'), t('I agree'))
  );
}

/**
 * Form validation handler for the current password on the user_account_form()
 *
 * @see user_account_form()
 */
function legalagree_user_form_validate($form, &$form_state) {
  global $user;

  // Did user agree?
  if ($form_state['input']['decision'] <> 1) {
    form_set_error('decision', t('You must agree to the Legal Agreement before registration can be completed.'));
  } else {
    watchdog('user', t('User %user agreed to legal terms', array('%user' => $user->name)));
  }
}

用户钩子在注册表单建立期间、表单校验期间及用户记录插入到数据库之后得到调用,我们简单的模块的结果在一个注册表单中看起来应该像图6-2。

Figure 6-2. A modified user registration form


使用profile模块收集用户信息

如果你计划扩展用户注册表单来收集关于用户的信息,你应该在写自己的模块之前试一试profile.module模块,它允许你建立任意的表单去收集数据,定义用户注册表单哪些信息是否必须收集,设计哪些信息是公用还是私有,此外它允许管理员去定义页面,那样用户就能选择一个URL来访问它们的profile了,URL结构site URL加profile/加profile字段名字加值。
例如,如果你定义一个文本profile字段叫profile_color,你能浏览喜欢黑色的那些用户http://example.com/?q=profile/profile_color/black,或者假设你建立一个会议站点,负责出席者的晚餐计划,你能定义一个复选框profile字段名叫profile_vegetarian,你就能通过羡慕的URL来看全部的素食者http://example.com/?q=profile/profile_vagetarian(注意是复选框字段,值是暗示的因此是忽略的;所以不像上课例子的black,这里没有值附加到URL末尾)。
一个真实世界的例子,参加2010旧金山Drupal大会的用户列表能用profile/conference-sf-2010访问到(这里,字段的名字没有前缀profile_)。

TIP:profile的概要页面的自动创建只有在profile设置表单填写了page title字段后才起作用,并且不可用于文本、URL、日期字段。

登录流程

登录处理开始于用户填上登录表单(典型的是http://example.com/?q=user或显示在一个区块中)并且点击了“Log in”按钮。
表单的校验例程检查用户名是否锁定、是否访问角色不允许访问、是否用户输入的用户名或密码无效,有这些状况时,用户都能得到及时通知。

注意:Drupal有本地和外部授权,外部授权例子有OpenID、LDAP、Pubcookie及其它。

Drupal通过在users表中查找出一行匹配用户名和密码hash的记录来实现登录本地用户。一个成功的登录结果引发两个用户钩子(login和load),你的模块可以实现他们如图6-3。

Figure 6-3. Path of execution for a local user login

      在载入时附加数据到$user

用户钩子的load操作是在$user对象成功从数据库载入时在响应调用user_load()内触发,这发生在一个用户登录、为一个节点抽取授权信息及其它几个点。

注意:因为调用用户钩子是昂贵的,user_load()在当前$user对象为一个请求实例化时是不调用的(查看$user节),如果你写你自己的模块,你要永远在调用一个期望完全载入$user对象的函数之前调用user_load(),除非你确认这些已经发生。

让我们写一个模块叫loginhistory来保存用户登录历史,我们将在用户的“My account”页面显示用户登录过的次数。在sites/all/modules/custom/创建目录loginhistory,并且增加下面几个文件:

loginhistory.info

name = Login History
description = Keeps track of user logins
package = Pro Drupal Development
core = 7.x
files[] = loginhistory.install
files[] = loginhistory.module

我们需要一个.install文件来建立数据库表存储登录信息:

loginhistory.install

<?php
/**
 * Implements hook_schema().
 */
function loginhistory_schema() {
  $schema['login_history'] = array(
    'description' => 'Stores infomation about user logins.',
    'fields' => array(
      'uid' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'description' => 'The {user}.uid of the user logging in.',
      ),
      'login' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'description' => 'Unix timestamp denoting time of login.',
      ),
    ),
    'indexes' => array('uid' => array('uid')),
  );
  return $schema;
}

loginhistory.module

<?php
/**
 * @file
 * Keep track of user logins.
 */

/**
 * Implement hook_user_login
 */
function loginhistory_user_login(&$edit, $account) {
  // insert a new record each time the user log in
  $nid = db_insert('login_history')->fields(array(
        'uid' => $account->uid,
        'login' => $account->login
  ))->execute();
}

/**
 * Implements hook_user_view_alter
 */
function loginhistory_user_view_alter(&$build) {
  global $user;

  // count the number of logins for the user.
  $login_count = db_query("SELECT count(*) FROM {login_history} where uid = :uid", array(':uid' => $user->uid))->fetchField();
  // update the user page by adding the number of logins to the page
  $build['summary']['login_history'] = array(
    '#type' => 'user_profile_item',
    '#title' => t('Number of logins'),
    '#markup' => $login_count,
    '#weight' => 10,
  );
}

然后安装这个模块,每个成功的用户登录都会引发hook_user_login的login操作,作为响应,模块在数据库的login_history表中插入一条新记录,用户访问“My account”页面,当$user对象在hook_user_view期间装入,hook_user_view_alter函数将触发,模块将在页面上增加用户的登录成功次数如图6-4。

Figure 6-4. Login history tracking user logins

      提供用户信息分类

如果你在drupal.org上有一个账号,你能在My account页面上看到提供的用户信息分类的效果了,然后选择Edit选项卡,去编辑你的账号信息,诸如密码,也可以提供关于你自己的几个其它分类信息如Drupal involvement、个人信息、工作信息及是否接收新闻。

外部登录

有时,你可能不想使用Drupal本地users表,例如你可能有一个用户的表在其它数据库或在LDAP内,Drupal很容易将外部授权整合进登录流程。
让我们实现一个是分简单的外部授权模块来描画外部收取如何工作。假设你的公司只聘任一个人叫Dave,并且用户名是基于first name和last name分配的,这个模块授权任何以dave开头的字符串为用户名的用户登录,如davebrown、davesmith、davejones将成功登录。我们的途径将用form_alter()去改变用户登录校验例程,变为运行我们自己的校验例程。这是sites/all/modules/custom/authdave/authdave.info:

name = Authenticate Daves
description = External authentication for all Daves.
package = Pro Drupal Development
core = 7.x
files[] = authdave.module

这是authdave.module

<?php

/**
 * Implements hook_form_alter().
 * We replace the local login validation handler with our own.
 */
function authdave_form_alter(&$form, &$form_state, $form_id) {
  // In this simple example we authenticate on username to see whether starts with dave
  if ($form_id == 'user_login' || $form_id == 'user_login_block') {
    $form['#validate'][] = 'authdave_user_form_validate';
  }
}

/**
 * Custom form validate function
 */
function authdave_user_form_validate($form, &$form_state) {
  if (!authdave_authenticate($form_state)) {
    form_set_error('name', t('Unrecognized username.'));
  }
}

/**
 * Custom user authentication function
 */
function authdave_authenticate($form_state) {
  // get the first four characters of the user name

  $username = $form_state['input']['name'];
  $testname = drupal_substr(drupal_strtolower($username), 0, 4);

  // check to see if the person is a dave
  if ($testname == 'dave') {
    // if it's a dave then user the external login_register function
    // to either log the person in or create a new account if that
    // person doesn't exist as a Drupal user
    user_external_login_register($username, 'authdave');
    return TRUE;
  } else {
    return FALSE;
  }
}

在这个authdave模块(见图6-5)我们简单地将第二个校验例程换为我们的,比较图6-5和图6-3,图6-3处理的是本地登录流程。

Figure 6-5. Path of execution for external login with a second validation handler provided by the authdave module (compare with Figure 6-3)

函数user_external_login_register()是帮助函数,在用户第一次注册这个用户并记录他的登录。用户davejones的假象登录路径显示为图6-6。

Figure 6-6. Detail of the external user login/registration process
如果用户名以dave开始,并且用户头一次登录,user表中没有这个用户的记录,这个记录将要增加。然而,这里不像Drupal默认的本地用户注册那样提供有效的e-mail地址,如果你的站点真的要给用户发邮件,这样的一个模块只是简单的而非真的解决方案。你可能想设置users表的email列,那样就将有一个与用户关联的e-mail地址,要做到这些,你能有自己的模块响应用户钩子的insert操作它将在一个新用户插入时触发:

/**
 * Implements hook_user_insert().
 */
function authdave_user_insert(&$edit, &$account, $category = NULL) {
  global $authdave_authenticated;
  if ($authdave_authenticated) {
    $email = mycompany_email_lookup($account->name);
    // set e-mail address in the users table for this user.
    db_update('users')
      ->fields(
        array(
          'mail' => $email,
        )
      )
      ->condition('uid', $account->uid)
      ->execute();
  }
}

已经理解的读者可能注意到:这里没有办法告诉用户是否是本地用户还是外部授权,我们聪明地保存一个全局变量包含我们模块做过的授权,我们能用如下方式查询authmap表:
db_query("SELECT uid FROM {authmap} WHERE uid = :uid AND module = :module", array(':uid' => $account->uid, ':module' => 'authdave'));
所有通过外部授权的用户在authmap表中都有一个记录,就像users表,然而,在这种情形授权和hook_user_insert运行于相同的请求,一个全局变量相较于数据库查询是个好的选择。


小结


读完本章,你能够去:

      + 理解在Drupal内部用户是如何呈现的
      + 理解怎样用几种方法存储用户相关数据
      + 钩进用户注册流程来从注册中的用户获取更多信息
      + 钩进用户登录流程以在用户登录时运行自己的代码
      + 理解外部授权用户怎样工作
      + 实现你自己的外部授权模块

更多的外部授权信息情况openid.module模块(Drupal核心一部分)或贡献模块pubcookie.module。