使用开源软件设计、开发和部署协作型 Web 站点,第 10 部分: 外部网 Web 站点的...

来源:百度文库 编辑:神马文学网 时间:2024/04/26 01:54:22
在这个文章系列中,在 IBM? Internet Technology Group 团队的带领下使用一套可免费获得的软件为虚构的 International Business Council(IBC)公司设计、开发和部署一个外部网 Web 站点。在本文中,学习如何定义外部网来满足客户需求,并研究创建外部网 Web 站点的实现技术。
在这个文章系列 中,团队为虚构的 International Business Council(IBC)公司创建一个外部网 Web 站点。这个 Web 站点必须有文档存储、讨论组、专门的工作组、会议日程安排、日程议题描述。
在本系列的第 2 部分 中,研究了设计阶段,这个阶段帮助我们查明了 IBC 的业务目标和用户目标。通过分析,我们获得了实现 IBC Web 站点的使用策略的一套需求,这样就定义了一个环境,IBC 成员可以在这个环境中查看机密资料并进行协作。这些需求包括:
每个成员必须通过身份验证,之后才能查看信息或进行交互。
如果一个成员是非活跃的(也就是说,他或她在一段预定义的时间段内没有与 Web 站点进行交互),成员的身份验证就失效了。
如果一个成员没有经过身份验证,那么除了与重新身份验证相关的信息之外,不显示任何信息。
在第一次成功地通过身份验证时,每个成员都需要同意依从使用条款和条件。
每个成员需要定期检查是否依从使用条款和条件。
外部网。一个使用互联网协议和网络连接的私有网络,它还可能使用公共电信系统,在供应商、生产商、合作伙伴、客户或其他业务单位之间安全地共享公司的信息或操作。
这些需求就组成了外部网的定义。在本文中,学习如何创建登录页面、会话过期并向用户提供在继续查看 Web 站点的其他内容之前需要了解的依从信息。您将使用在第 7 部分 中创建的主题模板,改编Automated Logout 模块,实现带警告的计时客户端注销,并开发一个新模块来实现依从确认系统。
我们希望我们构建的外部网函数可能令您感兴趣,也希望我们修改现有模块和创建新模块的方法对您有帮助。这里提供的信息不应该解释为严格的开发规则,而是应该作为构建自己的解决方案时的起点。现在,首先构建登录页面。
因为我们要创建一个 “互联网内的互联网”,所以需要确保这个私有网络之外的任何用户都看不到这个网络中的任何信息。当我们查看 IBC Web 站点时,应该只看到登录页面 —— 假设没有通过 cookie 或现代浏览器常常附带的身份验证存储工具进行自动的身份验证。(cookie 和密码存储实用程序超出了本文的范围。)图 1 显示 IBC 登录页面。

在第 7 部分 中,学习了如何根据 Drupal 用户是否通过了身份验证选择替代的模板文件。这涉及在 page.tpl.php 文件中非常接近开头的地方使用一些简单的 PHP 代码。当用户还未通过身份验证时,这段代码就会显示登录页面,如清单 1 所示。
uid) { include('login.tpl.php'); return; } ?>
只有在用户已经通过身份验证的情况下,uid 属性才会出现在 $user 对象中,所以这种方法可以可靠地通知 Drupal 主题系统使用另一个模板文件。在这个示例中,我们将登录页面模板命名为 login.tpl.php。
如图 1 所示,登录页面的外观与以前文章中显示的任何 IBC Web 页面布局都很不一样。除了 IBC 标志之外,只显示与身份验证相关的信息。login.tpl.php 模板文件几乎是纯粹的 XHTML。我们可以建立自己的登录表单,从而确保输入表单元素使用正确的值来触发 Drupal 身份验证系统。但是,我们选择让 Drupal 替我们创建表单,并将这个表单嵌入我们的 XHTML 结构中。
清单 2 中的代码演示如何使用module_invoke 函数安全地调用 user.module 中的user_login 函数,并显示我们需要的主题化登录表单。现在,在一个由主题控制的单独页面中有了一个 Drupal 生成的登录表单,这个页面只在用户还未通过身份验证的情况下显示。

Please log in






回页首
对于外部网 Web 站点来说,其部分信息或所有信息常常是敏感的,只应该让通过身份验证的用户看到。用户可能离开已经通过身份验证的浏览器,从而让别人有机会接触敏感信息;为了减少这种可能性,需要使用一种机制在一段指定的时间之后自动地注销用户。
最初,我们实现了一种解决方案,它在属于 Drupal 核心的 session.inc 文件中添加代码,这使我们能够在 HTTP 请求周期的早期添加钩子,从而在预定义的无活动时间段之后影响用户会话。我们的解决方案是有效的,但不是推荐的解决方案。在正常的开发周期 之外修改 Drupal 核心会导致很大的麻烦。在升级到新的 Drupal 版本时,这个问题尤其严重。
我们了解到,已经有一个补丁被提交给 Drupal 项目,它支持在 HTTPD请求周期 的启动阶段装载不同的 session.inc 文件。
与任何问题一样,实现自动注销也有许多方式。使用 PHP 会话超时设置无法提供足够的控制能力,所以实现自动注销有两种主要方式:服务器端和客户端。
服务器端
将判断用户多长时间没有活动并对他们进行注销的所有逻辑都放在服务器上。这样做的好处主要是增加了安全性。通过将代码放在服务器端,减少了通过跨站点脚本、SQL 注入等手段进行恶意活动的风险。缺点是只能针对每个请求检查计时。我们还考虑了通过一个每分钟的触发器使用cron 钩子来检查计时。
客户端
使用客户端解决方案意味着使用 JavaScript? 这样的技术将所有逻辑都放在客户端。当然,如果解决方案编写得不好的话,就容易遭到跨站点脚本攻击。这么做的好处是逻辑可以实时检查无活动的时间长度。还可以在快发生自动注销时提供警告。
一种组合式的方法是,由服务器端逻辑向客户端发出不可见的消息,在客户端使用 Ajax 解决方案接收这些消息来创建警告。
这个模块为开发我们自己的解决方案提供了一个良好的起点;我们首先在 Drupal 环境中安装这个模块。基于前面给出的所有考虑因素,我们希望使用服务器端或客户端解决方案来为 IBC 提供灵活性。我们注意到了Automated Logout 模块。这个模块提供了针对每个请求对用户的无活动时间进行计时的逻辑,如果无活动时间到达了预定义的时间,就注销用户(也就是,删除会话信息并将用户重定向到首页)。我们首先在 Drupal 环境中安装这个模块。
我们在 Automated Logout 模块中添加了几个特性,包括:
设置启用客户端脚本所需的所有数据库字段和变量。
配置客户端脚本来执行自动注销逻辑。
将服务器端变量传递给客户端脚本。
插入 JavaScript 对用户无活动时间进行计时,在自动注销之前的特定时间向用户发出警告,并让服务器注销用户的会话。
如果用户被自动注销了,当他们重新登录时,将他们重定向到 Web 站点中当前的位置。
现在,我们来创建支持自动注销的数据库表和变量。
根据我们的需求,我们需要许多额外的变量来支持用 autologout.module 实现这个新功能。这些变量包括:
表示是否使用客户端脚本的标志
要显示警告的时间
警告所用的文本
要将用户返回到的当前 URL(也就是自动注销之前的位置)
可以考虑使用 Drupal 目标变量。可以在表单中的一个隐藏输入元素中定义它,然后由drupal_goto 函数获取它,从而在提交表单之后提供重定向。我们认为可以使用这种方法将用户重定向到原来的位置,但是更简单的方法是将 URL 存储在 autologout 中,这样就能够在模块请求周期中的任何时刻或未来的请求中获取它。
第 9 部分 解释了模块安装文件的函数以及如何应用更新。清单 3 演示如何更新现有的 autologout 表,用一个更新函数在其中存储 URL。
function autologout_update_1() { global $db_type; switch ($db_type) { case 'mysql': case 'mysqli': db_query('ALTER TABLE {autologout} ADD COLUMN url VARCHAR(255) NOT NULL '); break; case 'pgsql': break; } }
如果已经安装了这个模块,Web 站点管理员可以运行 update.php 脚本来更新 autologout 表。如果这个模块是新安装的,Drupal 将运行更新函数,从而确保 autologout 表的模式是最新的。
可以在安装文件中使用variable_set 函数在变量表中创建新变量。但是,为了与 autologout.module 保持一致,我们将这些变量添加到在文件顶部定义的 autologout_default_settings 类对象中。添加的内容见清单 4。注意,$alert_text 变量包含以 % 开头的文本。后面将使用它们替换警告文本中的值。
class autologout_default_settings { ... var $clientsidetrigger = FALSE; // Initially disabled var $alert_time = 160; // default 2 minutes var $alert_text = 'ALERT! %user_name, you have been inactive for some time. '. 'You will be automatically logged out in %time_remaining seconds '. 'unless you interact with our web site.'; ... }
既然已经有了客户端脚本所需的变量,就需要让这些变量出现在模块设置页面上(admin/settings/autologout)。通过使用清单 5 所示的代码,可以添加一个启用客户端逻辑的复选框,以及用来修改警告时间和警告文本值的输入元素。
function autologout_settings() { ... $form['autologout']['clientsidetrigger'] = array( '#type' => 'checkbox', '#title' => t('Enable client side timeout'), '#default_value' => _autologout_local_settings('clientsidetrigger'), '#description' => t('Check this to allow javascript to trigger the '. 'auto logout instead of relying on the HTTP '. 'request mechanism. Enabling this disables any '. 'use of the browser refresh delta') ); $form['autologout']['alert_time'] = array( '#type' => 'textfield', '#title' => t('Alert time value in seconds'), '#default_value' => _autologout_local_settings('alert_time'), '#size' => 10, '#maxlength' => 12, '#description' => t('The length of time, in seconds, before auto logout '. 'when an alert is shown warning the user. This time '. 'needs to be smaller than the timeout setting if an '. 'alert is displayed. This settings works only when '. 'the client side trigger is enabled.') ); $form['autologout']['alert_text'] = array( '#type' => 'textarea', '#title' => t('Alert text'), '#default_value' => _autologout_local_settings('alert_text'), '#cols' => 60, '#rows' => 3, '#description' => t('The text to be used in the text alert dialog that '. 'warns the user of a pending auto logout. You can '. 'use the variables %timeout, %alert_time and %user_name.') ); ... }
这三个新的表单元素是用 Drupal Forms API 构建的,它们使用现有的 _autologout_local_settings 函数来获得用于填充表单元素的值。当 Drupal 处理设置表单时,如果有公用父数组名的话,就使用这个名称作为 variables 表中记录的名称字段;子名称和值将依次放进值字段。
例如,$form['autologout']['alert_text'] 这个元素包含父名称 autologout。在提交表单之后,Drupal 将键 alert_text 和与这个表单元素相关联的值串在一起,并使用 autologout 作为名称字段,串在一起的键/值对作为值字段,从而在变量表中设置一个记录。_autologout_local_settings 函数会把来自变量表的键/值对分解开,从而提供作为参数传递的设置键的值。
在稍后描述的 Compliance 模块中,将使用另一种方法组织 variable 表中存储的设置。
现在需要将服务器端变量传递给客户端 JavaScript。一种方法是在模块中以变量的形式创建 JavaScript,将变量替换进去,并将修改后的代码提供给 Drupal,从而包含在 Web 页面中。
另一种更常用的方法是在 Web 页面中创建新的命名容器,比如带 id 属性值的 DIV 元素,这样 JavaScript 就能够在运行时找到它们。主张纯语义标记的人可能不喜欢这种方式,而且如果具有这些额外标记的页面没有应用样式,就会显示出不希望的内容。在这里,我们将演示如何在闭包变量中创建隐藏的表单元素,在其中包含 JavaScript 所需的变量。尽管这仍然要添加非语义标记,但是在关闭样式时,内容至少会隐藏起来。
现有的 autologout.module 使用footer 钩子来包含逻辑,这些逻辑检查用户是否在指定的时间段内没有活动,如果出现这种情况,就关闭会话并将用户重定向到首页。清单 6 显示对这个现有函数的修改。
function autologout_footer() { ... $footer = ''; ... if (_autologout_local_settings('enabled')) { if (_autologout_local_settings('clientsidetrigger')) { $alert_text = t(_autologout_local_settings('alert_text'), array( '%timeout' => (int)_autologout_local_settings('timeout'), '%time_remaining' => (int)_autologout_local_settings('alert_time'), '%user_name' => check_plain($user->name) )); $form = array(); $form['clientsidetrigger']['timeout'] = array( '#type' => 'hidden', '#value' => (int)_autologout_local_settings('timeout'); ); $form['clientsidetrigger']['alert_time'] = array( '#type' => 'hidden', '#value' => (int)_autologout_local_settings('alert_time') ); $form['clientsidetrigger']['alert_text'] = array( '#type' => 'hidden', '#value' => check_plain($alert_text) ); $footer = drupal_get_form('autologout_clientside_trigger_js', $form); drupal_add_js(drupal_get_path('module', 'autologout') . '/clientsidetrigger.js'); } else { ... // existing logic for server side timing and logout ... } } return $footer; }
为了保留现有函数,但是允许选择客户端方法,我们将这些逻辑包围在条件语句中,使用 $clientsidetrigger 变量决定要运行的逻辑。
我们来看一下启用客户端函数的逻辑。首先,使用 t 函数构造 $alert_text。这样就可以将警告文本中的 %string 替换为它们代表的值。接下来,使用 Drupal Forms API 构建隐藏的表单元素,这些元素用来包含客户端逻辑要检查的参数。drupal_get_form 函数生成表单的 XHTML,这些 XHTML 将由 phptemplate 引擎放在闭包区域变量中。为了让这些内容出现在 Web 页面中,在 page.tpl.php 模板中定义的 XHTML 结构前面使用清单 7 中的代码。

使用drupal_add_js 函数将提供客户端逻辑的 JavaScript 提供给 Drupal,以便包含在 Web 页面中。drupal_get_path 函数帮助识别 autologout.module 中放置新的 JavaScript 文件的路径。
autologout.module 附带一个用 JavaScript 实现的简单的倒数计时器。这可以显示在一个块中,让用户可以看到会话什么时候将会超时。尽管可以将警告和注销逻辑添加在其中,但是我们决定将修改分隔开以简化实现。
客户端 JavaScript 的功能是创建一个倒数计时器,它从定义的超时值倒数到零,到达零时用户被重定向到对他们进行注销的 URL。在某个预定义的时间,向用户显示一个警告,指出如果他们再不与 Web 站点进行交互,就会被注销。
这一修改的客户端 JavaScript 包含在 clientsidetrigger.js 文件中,这个文件在与 autologout.module 相同的目录中。正如在前一节中看到的,使用 drupal_add_js 函数将它提供给 Drupal。这个脚本中所做的第一件事是,告诉 Drupal 在装载 Web 页面时运行它,如清单 8 所示。
if (isJsEnabled()) { addLoadEvent(autologoutClientsideTriggerStart); }
Drupal 4.7 提供了一些有用的 JavaScript实用程序。isJsEnabled 函数测试所需的 JavaScript 方法(比如 getElementsByTagName 和 getElementById)的可用性。如果没有这些方法,Drupal 环境提供的其他 JavaScript 函数可能是无效的。addLoadEvent 函数将一个 JavaScript 函数添加到 window.onload 事件上,这确保它在装载 Web 页面时运行。
在清单 8 中可以看到,autologoutClientsideTriggerStart 函数被添加到了 window.onload 事件上。
autologoutClientsideTriggerStart 对倒数计时逻辑进行初始化,如清单 9 所示。
var AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED = 0; var AUTOLOGOUT_CLIENTSIDE_TRIGGER_TIMEOUT_ID; function autologoutClientsideTriggerStart() { var countdown = parseInt(document.getElementById('edit-timeout').value); var alertTime = parseInt(document.getElementById('edit-alert_time').value); AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED = 1; autologoutClientsideTriggerCountdown(countdown,alertTime); }
这里定义两个全局变量,它们用来处理 setTimeout 函数创建的循环。可以看到如何将表单元素中的值作为 countdown 和 alertTime 变量传递给 JavaScript。我们设置全局循环变量,并调用 autologoutClientsideTriggerCountdown 函数来启动倒数计时器。
倒数计时器仅仅是一个在 JavaScript 应用程序中执行计时操作的简单的setTimout 结构。清单 10 显示创建计时器逻辑的代码。
function autologoutClientsideTriggerCountdown(countdown,alertTime) { if (countdown == 0) { clearTimeout(AUTOLOGOUT_CLIENTSIDE_TRIGGER_TIMEOUT_ID); AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED = 0; window.location.href = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + '/autologout/logout'; } if (countdown == alertTime) { autologoutClientsideTriggerAlert(); } if (AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED) { countdown --; AUTOLOGOUT_CLIENTSIDE_TRIGGER_TIMEOUT_ID = setTimeout('autologoutClientsideTriggerCountdown(' + countdown + ', ' + alertTime + ')', 1000); } }
autologoutClientsideTriggerCountdown 函数检查 $countdown 变量。如果它与 $alertTime 相同,那么使用 autologoutClientsideTriggerAlert 函数向用户显示警告。如果 $countdown 变量是零,那么将用户重定向到 URL /autologout/logout。
创建警告的逻辑放在一个单独的函数 autologoutClientsideTriggerAlert() 中。这意味着在构建警告时可以从输入字段元素一次装载警告文本。清单 11 显示了这个函数。
function autologoutClientsideTriggerAlert() { alert(document.getElementById('edit-alert_text').value); }
这是一个非常简单的警告,但是可以在 page.tpl.php 文件或使用 drupal_add_js 函数提供给 Drupal 的新 JavaScript 文件中用自己的函数覆盖这个 JavaScript 函数,从而创建更复杂的警告机制。
因为客户端逻辑调用 URL /autologout/logout,我们需要创建一个新的菜单项。现有的 autologout.module 需要一个新的菜单钩子来实现这个菜单项,如清单 12 所示。
function autologout_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array('path' => 'autologout/logout', 'title' => t('Autologut'), 'access' => TRUE, 'type' => MENU_CALLBACK, 'callback' => '_autologout_logout'); } return $items; }
现在,这个 URL 将调用 _autologout_logout 函数。现有的 autologout.module 在上面描述的 footer 钩子中执行注销逻辑。我们将这些代码放进一个单独的函数,并从 footer 钩子中原来的位置调用它,从而重用这些代码。这个函数称为 _autologout_logout;部分清单见清单 13。
为了方便用户,在用户重新通过身份验证时,应该将用户返回自动注销时所在的 Web 页面。这要使用数据库中的 URL 列。可以对 autologout.module 进行几处修改来创建这个特性:
确保对 autologout 表的所有 INSERT 操作都包含新的 URL 列。
在 _autologout_logout 函数中注销用户之前,将 URL 存储在 autologout 表中。
当用户重新登录之后,将用户重定向到 autologout 表中存储的 URL。
如果用户进行手工注销,就清空 autologout 表中存储的 URL。
autologout.module 中目前只有一个 INSERT 操作,它位于 user 钩子中执行更新操作的逻辑中。将这个操作改为将 URL 字段设置为空字符串。
为了在用户自动注销时存储 URL,我们将清单 13 中的代码添加到 _autologout_logout 函数中,放在将用户重定向到首页的 drupal_goto 调用前面。
function _autologout_logout() { ... global $user; $r = db_query("SELECT * FROM {autologout} WHERE uid = %d", $user->uid); if (db_num_rows($r) > 0) { $r = db_query("UPDATE {autologout} SET url = '%s' WHERE uid = %d", check_url($_SERVER["HTTP_REFERER"]), $user->uid); } else { $r = db_query("INSERT INTO {autologout} SET uid = %d, url = '%s'", $user->uid, check_url($_SERVER["HTTP_REFERER"])); } if (!$r) { watchdog('user', 'Unable to insert current url before auto logout', WATCHDOG_ERROR); } ... }
这是非常标准的代码,可以确保以用户 ID 作为键将 URL 存储在 autologout 表中。为了进行审计,如果遇到问题,就发出一条警告消息。
为了确保在用户重新登录之后将用户重定向到这个 URL,可以在现有的 user 钩子中使用登录操作。autologout.module 在现有的 user 钩子使用一个分支语句来识别触发函数调用的操作。清单 14 显示用来获得 URL 并将用户重定向到这个 URL 的分支语句。
function autologout_user($op, &$edit, &$account, $category = NULL) { ... case 'login': $q = db_query("SELECT url FROM {autologout} WHERE uid = %d", $account->uid); if ($r = db_fetch_object($q)) { if (isset($r->url) and $r->url != '') { drupal_goto($r->url); } } break; ...
如果成功地从 autologout 表中获得了与用户 ID 对应的 URL,drupal_goto 将用户重定向到这个 URL。
当然,如果用户从 Web 站点手工注销,那么就需要将为这个用户存储的所有 URL 重新设置为空字符串。为此,可以在现有的 user 钩子中使用 logout 操作。清单 15 显示 user 钩子中执行此操作的代码。
... case 'logout': $r = db_query("SELECT * FROM {autologout} WHERE uid = %d", $account->uid); if (db_num_rows($r) > 0) { $r = db_query("UPDATE {autologout} SET url = '' WHERE uid = %d", $account->uid); if (!$r) { watchdog('user', 'Unable to reset URL before logout', WATCHDOG_ERROR); } } break; ...
对 autologout.module 的这些修改启用了客户端超时控制和警告,以及在自动注销之前存储用户的当前 URL 的特性。
在下一节中,学习如何创建一个模块来向用户显示依从信息,并创建检查这些条款和条件的周期。




回页首
IBC 为 Web 站点维护一个使用策略,其中包含一组条款和条件,IBC 成员必须确认并同意这些条款和条件,之后才能与 Web 站点进行交互。还需要进行定期检查,以确保用户依从这些要求。
在创建任何新模块之前,一定要检查是否有现有的模块能够提供所需的功能,或者作为开发新模块的起点。我们考虑了Legal 模块,它的功能与新模块有些重叠。Legal 模块在帐号注册期间向读者提供 Terms and Conditions 页面,要求读者同意。但是,我们客户的过程更严格。在 IBC 管理团队创建成员帐号之后,新成员在第一次登录期间需要查看并同意依从页面,然后才能看到其他内容。无论如何,Legal 模块是一个良好的起点。
在用户登录之后,compliance 模块提供一个依从信息页面,如图 2 所示。

这个页面要求用户确认这些信息,并在他们同意之前限制他们与 Web 站点的交互。它记录哪些用户已经同意了依从信息,并创建图 3 所示的依从信息视图。这个模块还提供一个检查周期,定期确保 Web 站点用户确认了依从信息。第 6 部分 中解释了基本模块的构造过程,所以我们不再讨论模块的总体结构,而是集中于这个 compliance 模块特有的实现细节。

包含 compliance 模块数据的表的模式包含以下内容:
uid 以用户 ID 作为键
accept_date 用户最近一次接受依从信息的日期
expiration_date 用户需要重新确认依从信息的日期(如果启用了检查周期)
url 临时存储用户当前的 URL,用来将用户重定向回原来的位置
expiration_date 列其实是多余的,因为可以将检查时间偏移量与 accept_date 时间相加来决定这个时间。但是,使用这个列比较方便。
清单 16 给出在 mysql/mysqli 数据库中创建存储依从信息的表的代码。
function compliance_install() { global $db_type; $created = FALSE; switch ($db_type) { case 'mysql': case 'mysqli': $q = db_query(" CREATE TABLE IF NOT EXISTS users_compliance ( uid integer unsigned NOT NULL default '0', accept_date integer NOT NULL default '0', expiration_date integer NOT NULL default '0', url varchar(255) NOT NULL default '', PRIMARY KEY (uid) ); "); if($q) { $created = TRUE; } break; case 'pgsql': break; } _compliance_install_init(); if ($created) { drupal_set_message(t('Compliance module installation was successful.')); } else { drupal_set_message(t('Installation for the Compliance module was unsuccessful.')); } }
在创建这个表之后,调用 _compliance_install_init() 函数。这会用默认值对 compliance 模块使用的变量进行初始化,见清单 17。
function _compliance_install_init() { variable_set('compliance_page_node', 0); variable_set('compliance_page_tile', t('Terms and conditions')); variable_set('compliance_review_enabled', 0); variable_set('compliance_review_cycle_time', 365); variable_set('compliance_message',t('Please read the following and answer the '. 'question at the bottom of the page. Thank you.')); variable_set('compliance_question',t('Do you agree to these term and conditions?')); variable_set('compliance_positive_answer',t('Agree')); variable_set('compliance_negative_answer',t('Disagree')); variable_set('compliance_disagree_url', '/logout'); variable_set('compliance_reset', 0); }
variable_set 函数将这些变量及其值存储在变量表中。现在,当在 admin/modules 显示的模块列表中启用这个模块时,将创建这个数据库表并将模块的默认值安装进 Drupal 系统。如果具有适当的访问权限,就可以使用依从设置(admin/settings/compliance)编辑这些变量。
这些变量包括:
compliance_page_node 用来描述依从信息的页面的节点 ID。
compliance_page_title 当显示用户确认表单时,依从信息的标题。
compliance_review_enabled 用来启用或禁用检查机制的标志。
compliance_review_cycle_time 要求用户再次确认依从信息之前的时间(以天为单位)。
compliance_message 一个可选的消息,它放在用户确认表单中依从信息的顶部。
compliance_question 要求用户确认表单中的依从信息的问题。
compliance_positive_answer 确认按钮上使用的文本,用户按这个按钮就表示接受依从信息。
compliance_negative_answer 拒绝按钮上使用的文本,用户按这个按钮就表示拒绝依从信息。
compliance_disagree_url 如果用户拒绝依从信息,就将他们重定向到这个 URL。如果用户接受依从信息,就将他们重定向到原来的 Web 页面。
注意,variable_set 函数也可以将传递给它的数组串在一起。这样就可以为所有依从设置使用一个记录,而不是为每个变量建立一个记录。在获取设置时,可以使用drupal_unpack 函数分解这些值。
我们假设任何通过身份验证的用户都可以看到一个显示依从信息的表单,这个表单要求用户确认此信息。所需的访问限制仅仅是只允许管理员配置 compliance 模块并查看依从审计页面。审计页面显示哪些用户已经确认了依从信息。清单 18 给出实现这些访问权限的 perm 和 access 钩子。
/*** * Implementation of hook_perm */ function compliance_perm() { return array('administer compliance','audit compliance'); } /** * Implementation of hook_access(). */ function compliance_access($op, $node) { if ($op == 'view') { return user_access('access content'); } else if ($op == 'create' || $op == 'update' || $op == 'delete') { if(user_access('administer compliance')) { return true; } else { return false; } } else { return false; } }
包含依从信息的页面是作为基本页面节点创建的(/node/add/page)。对于 IBC Web 站点,我们使用 Drupal 4.7 附带的 Path 模块为依从信息的页面节点设置别名 URL /tncs(“terms and conditions” 的缩写)。在页面底部的链接中使用这个别名,让用户随时都可以查看 Web 站点的依从信息。
依从页面(/compliance)是 tncs 页面节点加上一些额外内容,让用户可以对依从信息进行响应。我们在清单 19 所示的菜单钩子中定义这个 URL。
这个菜单钩子中注册了另一个 URL(/compliance/audit)。这个页面显示哪些用户已经确认了依从信息,如图 3 所示。
注意,用来为依从审计页面提供样式的默认层叠样式表(CSS)文件是通过 theme_add_style 函数提供给 Drupal 系统的。
function compliance_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array('path' => 'compliance', 'title' => variable_get('compliance_page_title', t('Terms and conditions')), 'access' => user_access('access content'), 'type' => MENU_CALLBACK, 'callback' => 'compliance_display'); $items[] = array('path' => 'compliance/audit', 'title' => t("Compliance audit"), 'access' => user_access('audit compliance'), 'type' => MENU_CALLBACK, 'callback' => 'compliance_audit'); theme_add_style(drupal_get_path('module', 'compliance') . '/compliance.css'); } return $items; }
正如第 6 部分 所述,settings 钩子提供一个表单,用来交互式地管理模块的变量。这个钩子使用 Drupal Forms API 构建表单布局。compliance 模块的 settings 钩子中比较有意思的部分见清单 20。
表单部件之一创建一个 SELECT 表单元素,其中将页面节点以 OPTION 元素的形式列出。然后,可以选择其中之一作为依从信息页面。如果没有找到页面节点,就提示用户添加一个节点。
注意,每个元素的 $form 数组名与 variable_get 函数中的变量名匹配。当 Drupal 处理用 settings 构建的表单时,它使用这个数组名作为变量来存储相关联的表单元素值。所以,在 $form['autologout']['alert_time'] 中定义的表单元素的值将在变量表中作为 alert_time 存储。这种方式与 autologout_settings 函数中存储变量的方式稍有不同。
function compliance_settings() { ... $q = db_query('SELECT n.nid, r.title FROM node n INNER JOIN node_revisions r USING (vid) WHERE n.type="page"'); while ($r = db_fetch_object($q)) { $options[$r->nid] = t($r->title); $selected = $r->nid; } if (count($options) == 0) { drupal_set_message(t('Please %link that will display the compliance terms '. 'and conditions and then come back to this page to complete '. 'the compliance settings.', array('%link' => l(t('add a page'), 'node/add/page/')))); return; } ... $form['compliance_page']['compliance_page_node'] = array( '#type' => 'select', '#title' => t('Page'), '#default_value' => variable_get('compliance_page_node', $selected), '#options' => $options, '#description' => t('This is the page displayed to the user describing '. 'the compliance terms and conditions. You can create '. 'a %link if no existing pages are appropriate to use.', array('%link' => l(t('new page'), 'node/add/page/'))) ); $form['compliance_page']['compliance_page_title'] = array( '#type' => 'textfield', '#title' => t('Title'), '#default_value' => variable_get('compliance_page_title', t('Terms and conditions')), '#description' => t('This is the text of the title that will '. 'override the one that comes with the node chosen above.') ); $form['compliance_ack'] = array( '#type' => 'fieldset', '#title' => t('Acknowledgment'), '#weight' => -14, '#description' => t('So that we can confirm that the user has read '. 'and/or agrees to this compliance content, a question '. 'is presented to ask the user to acknowledge this '. 'compliance page before moving on. If a positive answer '. 'is given, the user is redirected to the original page '. 'they would have gone to. If a negative answer is given, '. 'then the user is redirected to the URL defined below.') ); $form['compliance_ack']['compliance_message'] = array( '#type' => 'textfield', '#title' => t('Message'), '#default_value' => variable_get('compliance_message', t('Please read the following and answer '. 'the question at the bottom of the page. ' 'Thank you.')), '#description' => t('This is the text of a message that will appear at '. 'the top of the compliance content intended to '. 'instruct the user to read the page and answer '. 'the question defined below.') ); ... }
如清单 19 所示,向 Drupal 菜单系统注册了 URL /compliance,从而触发 compliance_display 函数。清单 21 给出这个函数。我们希望使用创建的页面节点显示依从信息,但是要求从 compliance 模块控制它,以便使用 view 钩子这样的函数添加内容,比如确认表单。
function compliance_display() { $nid = variable_get('compliance_page_node', 0); if ($nid == 0 || !is_numeric($nid)) { drupal_not_found(); } $node = node_load($nid); if ($node->nid) { $title = check_plain(variable_get('compliance_page_title', t('Terms and conditions'))); drupal_set_title($title); $node->title = $title; $node->type = 'compliance'; return node_show($node, 'view'); } }
这个函数的前半部分通过 node_load 函数使用节点 ID 获得页面节点的节点对象。后半部分使用存储的标题文本覆盖页面和节点标题,将节点类型从 page 改为 compliance,然后使用 node_show 函数要求 Drupal 显示这个节点。
改变节点类型使 compliance 模块能够控制页面节点的内容。清单 22 演示如何使用 view 钩子添加内容。
function compliance_view(&$node) { $form = array(); $form['compliance_ack_question'] = array( '#type' => 'markup', '#value' => '

'.variable_get('compliance_question', t('Do you agree to these term and conditions?')).'

' ); $form['compliance_ack_disagree'] = array( '#type' => 'submit', '#value' => variable_get('compliance_negative_answer', t('Disagree')) ); $form['compliance_ack_agree'] = array( '#type' => 'submit', '#value' => variable_get('compliance_positive_answer', t('Agree')) ); $node->body .= drupal_get_form('compliance_ack', $form); drupal_set_message(variable_get('compliance_message', t('Please read the following and answer the '. 'question at the bottom of the page. '. 'Thank you.'))); return $node; }
这个表单提示用户确认依从信息,它是用 Form API 构建的。使用 drupal_get_form 函数将这个表单主题化成 XHTML。这个函数使用表单 ID(compliance_ack)设置处理表单检验和提交的回调函数(见下一节中的描述)。然后将这个 XHTML 表单追加到现有的页面(目前是依从页面)节点内容中。然后使用 drupal_set_message 函数将存储的消息添加到 Drupal 消息队列中。
使用 drupal_get_form 函数和表单 ID(compliance_ack)让 Drupal 寻找与这个表单 ID 相关联的提交和检验钩子函数。对于这个表单 ID,钩子函数分别称为 compliance_ack_submit 和 compliance_ack_validate。compliance 模块只使用 submit 钩子,如清单 23 所示。
function compliance_ack_submit($form_id, $form_values) { switch($_POST["op"]) { case variable_get('compliance_positive_answer', t('Agree')): $url = _compliance_get_destination(); _compliance_set_accept(); drupal_goto($url); break; case variable_get('compliance_negative_answer', t('Disagree')): drupal_goto(variable_get('compliance_disagree_url', '/logout')); break; } }
使用一个分支语句测试在提交表单之后在 $_POST 数组中传递回来的 $op 变量。如果用户给出了肯定的响应,就使用清单 24 所示的 _compliance_get_destination 函数获得原来请求的 URL(见下一节中的解释)。通过清单 25 所示的 _compliance_set_accept 函数,使用接受日期和过期日期更新 users_compliance 表中这个用户的记录。用户被重定向到原来请求的位置。如果用户给出了否定的响应,用户就被重定向到存储的指定位置。
清单 24 所示的 _compliance_get_destination 函数仅仅获取以前为这个用户存储的 URL 信息。如果没有找到 URL,就返回空字符串。将空字符串传递给 drupal_goto 函数,就会将用户重定向到首页。
function _compliance_get_destination($uid = NULL) { global $user; if ($uid == NULL) { $uid = $user->uid; } $url = ''; $q = db_query('SELECT url FROM {users_compliance} WHERE uid = %d', $uid); if (db_num_rows($q) > 0) { $r = db_fetch_object($q); $url = $r->url; } return $url; }
_compliance_set_accept 函数使用接受时间和过期时间在 users_compliance 表中更新或创建记录。过期时间只在启用了检查周期的情况下使用。
function _compliance_set_accept($uid = NULL) { global $user; if ($uid == NULL) { $uid = $user->uid; } $now = time(); $later = $now + (variable_get('compliance_review_cycle_time',365) * 60 * 60 * 24 ); db_query("DELETE FROM {users_compliance} WHERE uid = %d", $user->uid); db_query("INSERT INTO {users_compliance} SET uid = %d, accept_date = %d, ". "expiration_date = %d, url=''", $user->uid, $now, $later); watchdog('user', 'Compliance has been acknowledged', WATCHDOG_NOTICE); }
依从页面应该在用户刚刚通过身份验证(或者说登录进 Web 站点)时显示。compliance 模块要求用户确认依从信息,然后才能做其他事情。通过在 user 钩子中使用 login 操作(见清单 26)就可以实现这个需求。
function compliance_user($op, &$edit, &$account, $category = NULL) { if ($account->uid < 2) { return; // UID 0 or UID 1 not applicable } switch ($op) { case 'login': if (_compliance_expired()) { _compliance_set_destination(); drupal_goto('compliance'); } break; } }
因为 Drupal 系统的第一个用户是特殊的根用户,我们要确保不对他应用依从过程。通过使用一个分支语句测试操作变量 $op 中的 login 值,检查用户是否对依从信息给出了肯定的响应,并使用清单 27 所示的 _compliance_expired 函数检查用户是否需要再次查看并确认依从信息。
如果用户需要查看依从页面,就使用 _compliance_set_destination 函数存储他们的当前 URL,这样的话,在他们确认依从信息之后,就可以将他们重定向回这个位置。最后,将用户重定向到依从页面。
_compliance_expired 函数见清单 27。从 users_compliance 表获取用户记录中的过期日期字段。如果没有找到记录或者过期日期字段为零,用户就需要查看依从页面。如果启用了依从检查周期,而且过期日期值是过去的某一时间,用户也需要查看依从页面。
function _compliance_expired($uid = NULL) { global $user; if ($uid == NULL) { $uid = $user->uid; } $q = db_query('SELECT expiration_date FROM {users_compliance} '. 'WHERE uid = %d', $user->uid); if (db_num_rows($q) > 0) { $r = db_fetch_object($q); if (((time() > (int)$r->expiration_date)) && (variable_get('compliance_review_enabled', 0) == 1)) { // expired – need to ack again return true; } elseif ((int)$r-> expiration_date == 0) { // record exists but no previous ack return true; } } else { // no previous ack return true; } return false; }
_compliance_set_destination 函数(见清单 28)在 users_compliance 表中创建或更新用户的记录,确保将 URL 字段设置为当前 URL。
function _compliance_set_destination($uid = NULL) { global $user; if ($uid == NULL) { $uid = $user->uid; } $q = db_query('SELECT * FROM {users_compliance} WHERE uid = %d', $uid); $url = check_url(url($_GET["q"])); if (db_num_rows($q) > 0) { $q = db_query('UPDATE {users_compliance} SET url = "%s" WHERE '. 'uid = %d', $url ,$uid); } else { $q = db_query('INSERT INTO {users_compliance} '. '(uid, accept_date, expiration_date, url) '. 'VALUES (%d, %d, %d, "%s")', $uid, 0, 0, $url); } return $q; }
用 $_GET["q"] 值调用 url 函数来获得 URL,然后将 URL 传递给 check_url 函数。调用 check_url 的目的是防止跨站点脚本攻击导致的恶意使用。
使用 URL /compliance/audit 显示审计页面,如图 3 所示。这个 URL 导致调用清单 29 所示的 compliance_audit 函数。这个页面提供 user_compliance 表的主题化视图。在默认情况下,主题化输出突出显示还没有确认依从信息的用户,也就是需要再次查看依从信息的用户。
function compliance_audit() { $q = db_query('SELECT u.uid,u.name,uc.accept_date,uc.expiration_date '. 'FROM users_compliance uc RIGHT OUTER JOIN users u USING '. '(uid) ORDER BY u.name ASC'); $acknowledments = array(); while ($r = db_fetch_object($q)) { $acknowledments[] = $r; } return theme('compliance_audit', $acknowledments); }
将 users_compliance 表中的数据逐行提取到一个数组中,然后传递给主题函数来创建依从审计页面体中使用的 XHTML 内容。theme 函数钩子 compliance_audit 也传递给这个函数。通过使用第 7 部分 中描述的主题函数选择过程,Drupal 寻找一个适当的主题函数来创建 XHTML。
compliance.module 文件中提供了审计页面的默认主题函数 theme_compliance_audit,见清单 30。
function theme_compliance_audit($ack) { $header = array(t('User name'), t('Accept date'), t('Expiration date'),t('Status')); $rows = array(); foreach($ack as $row) { if ($row->uid < 2) { continue; } $class = 'compliance_okay'; $status = 'Okay'; $accept = ' - '; $expiration = ' - '; if ((time() > $row->expiration_date) && variable_get('compliance_review_enabled',0) == 1) { $class = 'compliance_expired'; $status = 'Expired'; } if (!isset($row->expiration_date)) { $class = 'compliance_unaccounted'; $status = 'Unaccounted'; } $name = l($row->name,'user/'.$row->uid); if (isset($row->accept_date)) { $accept = format_date($row->accept_date,'custom','l, F j, Y'); } if (isset($row->expiration_date)) { $expiration = format_date($row->expiration_date,'custom','l, F j, Y'); } $rows[] = array('data'=> array($name,$accept,$expiration,$status),'class'=>$class); } $output = '
'; $output .= '

Compliance audit

'; if (variable_get('compliance_review_enabled',0) == 1) { $output .= '

A review cycle of ' . format_plural(variable_get('compliance_review_cycle_time',365), 'a day','%count days') . ' has been enabled.

'; } $output .= theme('table', $header, $rows); $output .= '
'; return $output; }
在这里,将来自数据库的每行数据添加到 $rows 数组中,然后将它主题化成一个表格。使用 l 函数创建链接。注意,在 rows 数组中添加了类,用来帮助识别用户依从的不同状态。这些样式在 compliance.css 文件中定义,在前面描述的 menu 钩子中添加进系统。
当然,这只是默认主题函数,可以用另一个主题函数覆盖它,从而创建更精细的审计页面。




回页首
在本文中,了解了 IBC Web 站点的需求以及这些需求如何影响外部网站点。为了实现这些需求,学习了如何创建登录页面来防止未通过身份验证的用户访问 Web 站点中的内容,使用 Automated Logout 模块启用客户端注销计时器和警告,创建一个新模块来提供 Web 站点使用策略所需的依从机制。
在本系列的下一篇文章中,将了解 Drupal 的分类法系统,学习如何使用它对内容进行组织和导航。




回页首
作者希望感谢Moshe Weitzman、Károly Négyesi(也称为 “chx”)、Boris Mann 和Jeff Robbins 提供的意见和建议。
学习
您可以参阅本文在 developerWorks 全球站点上的英文原文 。
本系列的 RSS feed。(了解关于RSS 的更多知识。)
关于外部网 的基础知识。
与本文相关的 Drupal 文档如下:
Automated Logout module
module_invoke()
user_login()
http://drupal.org/node/316">Developing for Drupal
hook_cron()
drupal_goto()
variable_set()
Forms API reference
hook_footer()
t()
drupal_add_js()
drupal_get_path()
http://drupal.org/node/42779">drupal.js Functions
setTimeout()
drupal_goto()
theme_add_style()
node_load()
node_show()
drupal_set_message()
url()
check_url()
theme()
l()
随时关注developerWorks 技术活动和网络广播。
学习来自 developerWorks 的架构专区 参考资料,包括 IBM 专家为这种新兴的 IT 学科提供的文章、新闻、论坛和 blog。
在technology bookstore 上浏览关于这些主题和其他技术主题的图书。
获得产品和技术
使用IBM 试用软件 构建您的下一个开发项目,这些软件可以从 developerWorks 直接下载。
讨论
参与论坛讨论。
通过参与developerWorks blog 加入developerWorks 社区。
关于 IBM     隐私条约     联系 IBM     使用条款
使用开源软件设计、开发和部署协作型 Web 站点,第 10 部分: 外部网 Web 站点的... 使用开源软件设计、开发和部署协作型 Web 站点,第 5 部分: Drupal 入门 使用开源软件设计、开发和部署协作型 Web 站点,第 13 部分: Eclipse 中的 ... 使用开源软件设计、开发和部署协作型 Web 站点,第 14 部分: announcemen... 使用开源软件设计、开发和部署协作型 Web 站点,第 2 部分: 设计有效的用户体验 使用开源软件设计、开发和部署协作型 Web 站点,第 12 部分: 主机托管和部署 使用开源软件设计、开发和部署协作型 Web 站点,第 12 部分: 主机托管和部署 使用开源软件设计、开发和部署协作型 Web 站点,第 11 部分: 使用 Drupal 中... 使用开源软件设计、开发和部署协作型 Web 站点,第 8 部分: 使用 CSS 对主题化内... 使用开源软件设计、开发和部署协作型 Web 站点,第 1 部分: 简介和概述 使用开源软件设计、开发和部署协作型 Web 站点,第 4 部分: 在 Linux 中建立开... 使用开源软件设计、开发和部署协作型 Web 站点,第 6 部分: 在 Drupal 中构建... 使用开源软件设计、开发和部署协作型 Web 站点,第 3 部分: 在 Windows 中建... 使用开源软件设计、开发和部署协作型 Web 站点 使用 Web 标准生成 ASP.NET 2.0 Web 站点 使用WebLogic将Web站点转换为Web服务(一) Deep Web 研究站点 使用 Web 标准生成 ASP.NET 2.0 Web 站点 - jelink的专栏 - ... 用WebSphere监视Web站点的性能 Web 站点崩溃的原因总结 .NET开发资源站点和部分优秀.NET开源项目 一个IP建多个Web站点 使用Ant进行Web开发(第二部分) XHTML CSS制作样式风格切换的WEB站点