第11章 处理用户输入:drupal过滤器系统

老葛的Drupal培训班 Think in Drupal

如果你需要手工的对输入信息进行格式化,那么向网站添加内容将是一个繁琐的工作。相反,如果想让网站上的文本内容看起来很漂亮,那么你需要懂得HTML---但是大多数用户都不了解这一知识。对于我们中的那些熟悉HTML的人,如果在头脑风暴会议或者文章创作期间,不断的暂停,并向我们的文章中插入HTML标签,这也会令人头痛。段落标签、链接标签、换行标签。。。太烦了。幸好,Drupal提供了预制的程序,这就是过滤器,它使得数据输入更加方便和高效。过滤器执行文本处理,比如为URL添加链接,将换行符转化为<p> 和 <br />标签,甚至包括过滤有害的HTML。钩子hook_filter()负责创建过滤器和处理用户提交的数据。
 

Drupal版本:

drupal过滤器

老葛的Drupal培训班 Think in Drupal

过滤器几乎总是一个单独的动作,比如“删除所有的超链接”、“为这篇文章添加一个随机图片”,或者甚至“将它翻译为pirate-speak”(参看pirate.module  http://drupal.org/project/pirate)。如图11-1所示,过滤器获取某种类型的文本输入,处理文本输入,并返回输出。
11-1.过滤器按照某种方式对文本进行转换,并将转换后的文本返回。
 
    过滤器的一个常见用途就是从用户提交的输入中删除不想要的标识文本。图11-2展示了工作时Drupal的HTML过滤器。
11-2. HTML过滤器仅允许特定的标签通过。这个过滤器主要用来阻止跨站点脚本攻击。
 

Drupal版本:

drupal过滤器和输入格式

老葛的Drupal培训班 Think in Drupal

假定你已经理解了过滤器是做什么的,而且你知道自己想找的是已安装的过滤器列表,你还是不能直接的在管理界面中找到它。为了让过滤器执行它们的工作,你必须将其分配给一个Drupal输入格式,如图11-3所示。输入格式将过滤器组合在一起,这样在处理内容时可以把它们作为批处理一同运行。与为每个提交选择一些过滤器相比,这要容易很多。你可以导航到“管理➤站点配置➤输入格式”,通过配置一个已有的输入格式或者新建一个输入格式,这样就可以查看已安装的过滤器列表了。
 
提示 一个Drupal输入格式是由一组过滤器构成的。
 
11-3. 已安装的过滤器列表位于 “添加输入格式”表单中
 
Drupal自带了3中输入格式(参看图11-4):
 
Filtered HTML输入格式由4个过滤器构成:
    HTML校正者过滤器,用来确保所有的HTML标签都正确的关闭和嵌套了。
    HTML过滤器,用于限制HTML标签以阻止跨站点脚本攻击(通常简称为 XSS);
    换行转换器,用于将回车转换为它们的HTML对应标签;
    URL过滤器,用于将web和电子邮件地址转化为超链接。
 
Full HTML 输入格式,它不对HTML进行任何限制,但它使用了换行转换器
 
PHP Code 输入格式由一个名为“PHP求值器”的过滤器构成它负责执行节点中的任意PHP脚本。一个好的经验法则是,永远不让用户使用带有PHP求值器”的输入格式。如果用户可以运行PHP的话,那么他们可以做任何PHP能做到的事情,包括让你的站点当掉,或者更糟糕的是删除了你的所有数据。为了避免这一可能性,Drupal自带的PHP求值器”过滤器默认是禁用的。如果你想使用它,那么需要启用PHP过滤器模块。
 
警告 在你的站点上,为任何用户启用PHP Code输入格式都会带来安全隐患。一个好的原则是不使用这一输入格式。如果你必须使用它,那就尽可能的少用,而且只给超级用户使用(用户ID为1的用户)。
 
11-4 Drupal自带了3种可配置的输入格式
 
    因为输入格式就是一组过滤器,所以它们是可扩展的。你可以添加和删除过滤器,如图11-5所示。你可以修改输入格式的名字,删除一个过滤器,甚至可以对输入格式中过滤器的执行顺序进行重新排列以避免冲突。例如,你可能想在运行HTML过滤器的前面运行URL过滤器,这样HTML过滤器就可以检查由URL过滤器生成的<a>标签了。
 
注意 输入格式(过滤器组)可以通过管理界面进行控制。开发者在定义过滤器的时候不用考虑输入格式。该工作由Drupal站点管理员负责。
11-5. 输入格式由一组过滤器构成。本图展示了Drupal默认的3个输入格式。执行的方向如箭头所示。

Drupal版本:

安装过滤器

老葛的Drupal培训班 Think in Drupal

安装过滤器与安装模块的流程是一样的,这是因为过滤器就是存在于模块文件中。因此,只需要在“管理➤站点构建➤模块”下面启用或者禁用相应的模块,这样就可以启用或禁用过滤器了。一旦安装了过滤器,就可以导航到“管理➤站点配置➤输入格式”,将新添的过滤器分配到你选的输入格式中了。图11-6展示了过滤器和模块之间的关系。
11-6 过滤器是作为模块的一部分创建的
 

Drupal版本:

知道什么时候使用过滤器

老葛的Drupal培训班 Think in Drupal

如果可以使用已有的钩子来处理文本,那么你可能会想,为什么此时还需要使用过滤器呢?例如,使用hook_nodeapi()为URL添加超链接,这也非常方便,比URL过滤器还好用。但是考虑一下这种情况----你需要为节点的主体字段使用5个不同的过滤器。现在假定你查看默认http://example.com/?q=node页面,它一次显示10个节点。那么算一下,现在为了显示这个页面,你就需要运行50个过滤器,而对文本的过滤操作是很费资源的。这同时也意味着无论什么时候调用一个节点,即便是正被过滤的文本未被修改,那么也需要运行这些过滤操作。你在一次又一次的没有必要的运行这一操作。
    过滤器系统有一个缓存层,极大的提升了性能。一旦为给定的文本片段运行完了所有的过滤器,该文本的过滤版本将被存储在cache_filter表中,在文本被再次修改前缓存的内容保持不变(使用过滤内容的MD5哈希值来判断是否被修改)。现在让我们回到前面的例子中,当文本未被修改时,我们就可以绕过过滤器直接从缓存表中加载10个节点的数据了---速度快多了!图11-7给出了过滤器系统处理流程的概览。
 
提示 MD5是一个算法,用来计算文本字符串的哈希值。Drupal使用它作为数据库中的一个高效的索引列,用来查找节点的过滤数据。
11-7 文本过滤系统的生命周期
 
提示 对于包含大量内容的站点,通过将过滤器缓存移到一个内存缓存中,比如memcached,那么可以极大地提升性能。
 
     现在你应该比刚才更聪明一点了,你可能会想,“好吧,如果在我们的nodeapi钩子中直接将过滤后的文本保存到node表中,那不更好吗?它的运行结果和过滤器系统可是一样的啊?” 尽管这一方法也解决了性能问题,但是你破坏了Drupal架构的一个基本原则:永不修改用户的原始数据。假定你的一个初级用户回过头来想编辑一篇文章时,当他看到很多内容都被包含在了HTML标签中时,我想他十有八九会向你打电话寻求支持的。过滤器系统的目标就是保持原始数据不变,同时可以在Drupal框架其余部分中使用过滤数据的缓存拷贝。这一原理应用在Drupal的各个API中,你将会经常看到。
 
注意 即便是禁用了Drupal页面缓存,过滤器系统仍将缓存它的数据。如果你看到的还是以前的过滤数据,那么可以导航到“管理➤站点配置➤性能”,点击底部的“清除缓存数据”按钮,来清空cache_filter表。
 

Drupal版本:

创建一个自定义过滤器

老葛的Drupal培训班 Think in Drupal

当然,Drupal过滤器可以创建链接,格式化你的内容,将文本转换为pirate-speak,但是它能够聪明到帮我们写日志么,或者至少能够把我们创造性火花碰撞出来么?当然,它可以做到这一点!让我们创建一个带有过滤器的模块,用来向日志条目中插入随机的句子。我们将启用这一模块,这样当你在编写文章中绞尽脑汁毫无灵感并需要一些火花时,你可以简单的键入[juice!],当你保存文章时,它将会被替换为一个随机生成的句子。如果你需要更多的智慧火花时,你可以在一个文章中多次插入[juice!]标签。
 
    在sites/all/modules/custom/下面创建一个名为creativejuice的文件夹。首先,向creativejuice文件夹下面添加creativejuice.info文件:
 
; $Id$
name = Creative Juice
description = Adds a random sentence filter to content.
package = Pro Drupal Development
core = 6.x
 
    接着,创建creativejuice.module文件,并将其添加到creativejuice文件夹下:
 
<?php
// $Id$
/**
 * @file
 * A silly module to assist whizbang novelists who are in a rut by providing a
 * random sentence generator for their posts.
 */

Drupal版本:

实现hook_filter()

老葛的Drupal培训班 Think in Drupal

现在模块的基本部分已经有了,让我们在creativejuice.module中添加我们的hook_filter()实现:
 
/**
 * Implementation of hook_filter().
 */
function creativejuice_filter($op, $delta = 0, $format = -1, $text = '') {
    switch ($op) {
        case 'list':
            return array(
                0 => t('Creative Juice filter')
            );
 
        case 'description':
            return t('Enables users to insert random sentences into their posts.');
 
        case 'settings':
            // No settings user interface for this filter.
            break;
 
        case 'no cache':
            // It's OK to cache this filter's output.
            return FALSE;
 
        case 'prepare':
            // We're a simple filter and have no preparatory needs.
            return $text;
 
        case 'process':
            return preg_replace_callback("|\[juice!\]|i",
                'creativejuice_sentence', $text);
 
        default:
            return $text;
    }
}
 
    过滤器API经过多个阶段,从收集过滤器的名字,到缓存,再到进行真实操作的处理阶段。通过检查creativejuice_filter(),让我们看一下这些阶段或者操作。下面是对这个钩子函数中参数的分解:
 
• $op:要执行的操作。在接下来的部分,我们将对它进行详细讨论。
 
• $delta: 一个实现hook_filter()的模块可以提供多个过滤器。你使用$delta来追踪当前执行的过滤器的ID。$delta是一个整数。由于creativejuice模块仅提供了一个过滤器,所以我们可以忽略它。
 
• $format:一个整数,表示正被使用的是哪一个输入格式。Drupal对这些的追踪是在filter_formats数据库表中进行的。
 
• $text:将被过滤的内容。
 
    根据$op参数的不同,可执行不同的操作。

Drupal版本:

实现hook_filter()(1)

老葛的Drupal培训班 Think in Drupal

list操作
    list操作返回的是一个包含滤器名字的关联数组,其中以数字为键,这是由于在单个hook_filter()钩子实例中可以声明多个过滤器的缘故。这些数字键可用在接下来的操作中,并可通过$delta参数传递回钩子。
 
case 'list':
return array(
0 => t('Creative Juices filter'),
1 => t('The name of my second filter'),
);
 
description操作
    该操作返回了一个简短描述,用来描述过滤器能做什么。只有具有“管理过滤器”权限的用户才能看到这些描述。
 
case 'description':
switch ($delta) {
case 0:
return t('Enables users to insert random sentences into their posts.');
case 1:
return t('If this module provided a second filter, the description
 for that second filter would go here.');
 
// Should never reach here as value of $delta never exceeds
// the last index of the 'list' array.
default:
return;
}
 
settings操作
     当过滤器需要一个用于配置的表单界面时,使用该操作。它返回一个表单定义。当表单提交时,将会使用variable_set()自动的将值保存起来。这意味在取回值时使用variable_get()。该操作的使用实例,可参看modules/filter/filter.module中的filter_filter()。
 
no cache操作
    当使用这个过滤器时,过滤器系统应该为过滤文本绕过它的缓存机制么?如果要禁用缓存,那么返回的代码应该为TRUE。在你开发过滤器时,你将需要禁用缓存,这样调试起来方便一些。如果你修改no cache操作所返回的布尔值,在生效以前,你需要编辑一个使用了你的过滤器的输入格式,这是因为编辑输入格式将更新filter_formats表和该过滤器的缓存设置。
 
警告 禁用单个过滤器的缓存,会删除使用了该过滤器的任意输入格式的缓存。
 
prepare操作
    内容的实际过滤流程包含两步。首先,允许过滤器准备待处理文本。这步的主要目的是将HTML转变为相应的实体。例如,有这样一个过滤器,它允许用户粘贴代码片段。prepare步骤会将代码转变为HTML实体,这样就可以阻止接下来的过滤器将它解释为HTML了。如果没有这一步的话,HTML过滤器将会清除掉这个HTML。过滤器使用prepare的例子,可参看codefilter.module,该模块用于处理<code></code> 和 <?php ?> 标签,从而允许用户发布代码而不用担心转义为HTML实体。该模块的下载地址为http://drupal.org/project/codefilter
 
process操作
     在process操作期间,从prepare步骤里面返回的结果将被传递回hook_filter()中。在这里进行实际的本文处理:将URL转换为可点击的超链接,删除恶意数据,添加单词定义,等等。在prepare操作和process操作中都应该返回$text
 
default操作
    包含默认情况非常重要。如果你的模块没有实现一些操作时,那么将调用该操作,要保证在这里总能返回$text(你的模块要过滤的文本)。
 

Drupal版本:

助手函数

老葛的Drupal培训班 Think in Drupal

当$op 为“process”时,每出现一次[juice!]标签你都要执行一个名为creativejuice_sentence()的助手函数。将这个函数也添加到creativejuice.module中。
 
/**
 * Generate a random sentence.
 */
function creativejuice_sentence() {
    $phrase[0][] = t('A majority of us believe');
    $phrase[0][] = t('Generally speaking,');
    $phrase[0][] = t('As times carry on');
    $phrase[0][] = t('Barren in intellect,');
    $phrase[0][] = t('Deficient in insight,');
    $phrase[0][] = t('As blazing blue sky poured down torrents of light,');
    $phrase[0][] = t('Aloof from the motley throng,');
 
    $phrase[1][] = t('life flowed in its accustomed stream');
    $phrase[1][] = t('he ransacked the vocabulary');
    $phrase[1][] = t('the grimaces and caperings of buffoonery');
    $phrase[1][] = t('the mind freezes at the thought');
    $phrase[1][] = t('reverting to another matter');
    $phrase[1][] = t('he lived as modestly as a hermit');
 
    $phrase[2][] = t('through the red tape of officialdom.');
    $phrase[2][] = t('as it set anew in some fresh and appealing form.');
    $phrase[2][] = t('supported by evidence.');
    $phrase[2][] = t('as fatal as the fang of the most venomous snake.');
    $phrase[2][] = t('as full of spirit as a gray squirrel.');
    $phrase[2][] = t('as dumb as a fish.');
    $phrase[2][] = t('like a damp-handed auctioneer.');
    $phrase[2][] = t('like a bald ferret.');
 
    foreach ($phrase as $key => $value) {
        $rand_key = array_rand($phrase[$key]);
        $sentence[] = $phrase[$key][$rand_key];
    }
 
    return implode(' ', $sentence);
}

Drupal版本:

drupal钩子:hook_filter_tips()

老葛的Drupal培训班 Think in Drupal

你使用creativejuice_filter_tips()来向终端用户显示帮助文本。在默认情况下,显示一个短消息,其中带有一个指向http://example.com/?q=filter/tips的链接,而链接页面则包含了所有过滤器的详细说明。
 
/**
 * Implementation of hook_filter_tips().
 */
function creativejuice_filter_tips($delta, $format, $long = FALSE) {
    return t('Insert a random sentence into your post with the [juice!] tag.');
}
 
    在前面的代码中,无论是简洁的帮助文本还是详细的帮助文本,都使用了相同的文本,如果你想返回一个更详细的文本解释时,你可以检查$long参数,如下所示:
 
/**
 * Implementation of hook_filter_tips().
 */
function creativejuice_filter_tips($delta, $format, $long = FALSE) {
    if ($long) {
       // Detailed explanation for http://example.com/?q=filter/tips page.
       return t('The Creative Juice filter is for those times when your
           brain is incapable of being creative. These times come for everyone,
           when even strong coffee and a barrel of jelly beans do not
           create the desired effect. When that happens, you can simply enter
           the [juice!] tag into your posts...'
       );
    }
    else {
        // Short explanation for underneath a post's textarea.
        return t('Insert a random sentence into your post with the [juice!] tag.');
    }
}
 
     一旦在模块列表页面启用了这个模块,那么就可以使用creativejuice过滤器了,你可以将它应用在一个已有的输入格式中,也可以应用在一个新建的输入格式中。例如,将creativejuice过滤器添加到Filtered HTML输入格式中以后,节点编辑表单中的“输入格式”部分,将如图11-8所示。
 
11-8. Filtered HTML输入格式现在包含了creativejuice过滤器,前面的节点编辑表单中的“输入格式”部分给出了相应指示。
 
    你可以使用合适的输入格式创建一个日志条目,然后提交包含[juice!]标签的文本:
 
Today was a crazy day. [juice!] Even if that sounds a little odd,
it still doesn't beat what I heard on the radio. [juice!]
 
     提交的内容将被转换为如下所示的内容:
Today was a crazy day! Generally speaking, life flowed in its accustomed stream
through the red tape of officialdom. Even if that sounds a little odd, it still
doesn't beat what I heard on the radio. Barren in intellect, she reverted to another
matter like a damp-handed auctioneer.
 

Drupal版本:

阻止恶意数据

 

如果你想阻止恶意的HTML,可以使用Filtered HTML过滤器对内容进行过滤,从而阻止XSS攻击。如果在一些情况下,你不能使用Filtered HTML过滤器,那么你可以使用下面的方式来手工的过滤XSS:
 
function mymodule_filter($op, $delta = 0, $format = -1, $text = '') {
switch ($op) {
case 'process':
// Decide which tags are allowed.
$allowed_tags = '<a> <em> <strong> <cite> <code> <ul> <ol> <li>';
return filter_xss($text, $allowed_tags);
default:
return $text;
break;
}
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

读完本章后,你应该可以:
• 理解什么是过滤器,什么是输入格式,以及如何使用它们转换文本
• 理解为什么过滤器系统比在其它钩子中执行文本处理更加有效
• 理解输入格式和过滤器的工作原理
• 创建一个自定义过滤器
• 理解drupal过滤器中的各种操作是如何工作的
 

Drupal版本: