第8章 最佳开发实践

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

在本章中,我们主要探讨Drupal开发中的一些最佳实践,遵守这些实践,能够帮助我们提高代码的质量,提高代码的安全系数,同时有利于开发者之间相互交流。我们首先学习Drupal的编码规范,以及为模块创建文档;接着学习如何编写安全的代码;介绍了常用的版本控制,并展示了如何在drupal.org上维护一个模块。


Drupal版本:

10 字符串连接

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

  在点号两边分别使用空格:

<?php 
  $string = 'Foo' . $bar;
  $string = $bar . 'foo';
  $string = bar() . 'foo';
  $string = 'foo' . 'bar';
?>

 


Drupal版本:

11 常量

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

PHP常量应该全部大写,并使用下划线来分隔字词:

 

/**

/**

 * Node is not published.

 */

define('NODE_NOT_PUBLISHED', 0);

常量的名字也应该使用它们的模块名作为前缀,这样就可以避免常量之间的命名冲突。例如,上面的NODE_NOT_PUBLISHED,此时就没有使用NOT_PUBLISHED


Drupal版本:

12 全局变量

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

在你自己的模块中,最好不要定义全局变量。当然我们可以使用Drupal自带的全局变量,比如$user, $language.比如(摘自node.module中的node_object_prepare函数)

global $user;

$node->uid = $user->uid;

$node->created = REQUEST_TIME;


Drupal版本:

14 PHP注释

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

Drupal实现了大多数的Doxygen注释规范。所有的文档必须使用下面的语法:

 

/**

 * Documentation here.

 */

 

除了第一行以外,其它各行在星号(*)前面必须要有一个空格。

 

注意 Doxygen是一个能够友好支持PHP的文档生成器。它从代码中提取PHP注释并生成适合用户阅读的文档。更多信息,可参看http://www.doxygen.org

 

当为一个函数添加说明文档时,对应文档必须紧挨着放在函数前,中间不能存在空行。

 

Drupal能够理解下面所列的Doxygen结构;我们接下来会对其中的大多数作简单的介绍,不过有关它们的更多信息,请参看Doxygen的官方站点。

 

• @mainpage

• @file

• @defgroup

• @ingroup

• @addtogroup (as a synonym of @ingroup)

• @param

• @return

• @link

• @see

• @{

• @}

 

遵循这些标准的好处是,你可以使用第3方的API模块,为你的模块自动生成文档。API模块实现了Doxygen文档生成器规范的一个子集,并专门针对Drupal代码的文档生成进行了优化。访问http://api.drupal.org,你就可以看到这个模块的一个实例,除了官方的这个站点以外,还有http://drupalcontrib.org/,里面包含了常用第三方模块的文档。而关于API模块的更多信息,可参看http://drupal.org/project/api


Drupal版本:

15 文档例子

 作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们以node.module文件为例,来学习一下模块文档的编写。我们按照从上到下的顺序,对不同的类型的文档做出对应的解释。

在模块的第3行(在<?php开始标签下面),包含了一段文档,它用来介绍这个模块是做什么的。文档的内容如下所示:

 

/**

 * @file

 * The core that allows content to be submitted to the site. Modules and

 * scripts may programmatically submit nodes using the usual form API pattern.

 */

     上面的意思是说,这个模块是用来向站点提交内容的。通过使用表单API的方式,第三方模块也可以通过程序的方式提交节点。

    在node.pages.in文件的开头,也存在类似的文档:

/**

 * @file

 * Page callbacks for adding, editing, deleting, and revisions management for content.

 */

    上面的意思是说,node.pages.in文件,里面包含了各种页面回调函数,比如节点添加、编辑、删除,以及修订本管理对应的页面。

    通过上面的两个例子可以看出,@file指令就是对当前文件的用途做出说明。


Drupal版本:

16 为常量编写文档

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

PHP常量应该大写,可以使用下划线分隔单词。当定义PHP常量时,最好能够解释一下它们是用来做什么的,如下面的代码片段所展示的这样:

 

/**

 * Node is not published.

 */

define('NODE_NOT_PUBLISHED', 0);

    这里定义了一个NODE_NOT_PUBLISHED常量,值为0。上面的注释用来说明常量的含义:节点未发表。


Drupal版本:

17 为钩子实现编写文档

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

当一个函数是一个钩子实现时,此时不需要为钩子编写文档。简单的说明一下实现了哪个钩子就可以了,例如:

 

/**

 * Implements hook_help().

 */

function node_help($path, $arg) {

   …

}

以及:

/**

 * Implements hook_theme().

 */

function node_theme() {

   …

}

    换成中文的形式,我通常这样写:

/**

 * 实现钩子 hook_help().


Drupal版本:

18 为函数编写文档

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

函数文档应该使用下面的语法:

 

/**

 * Extract the type name.

 *

 * @param $node

 *   Either a string or object, containing the node type information.

 *

 * @return

 *   Node type of the passed-in data.

 */

function _node_extract_type($node) {

  return is_object($node) ? $node->type : $node;

}

    在文档的开头,我们首先描述一下这个函数是用来做什么的。如果你是用英语写文档的话,注意这里使用祈使语句。

 

    这里我们用到了两个文档指令, @param用来给出函数的参数,一个函数文档可以包含多个@param@return用来表示函数的返回值,通常只有一个。

   此外,我们还会用到其它一些文档指令,比如@ingroup

/**

 * Saves a node.

 *

 * @ingroup actions

 */

function node_save_action($node) {

  …

}

    它可以将一组函数关联在一起。node.module中有多个函数用到了@ingroup actions

/**

 * Sets the promote property of a node to 0.

 *

 * @ingroup actions

 */

function node_unpromote_action($node, $context = array()) {

  …

}

    在http://api.drupal.org/api/drupal/includes--actions.inc/group/actions/7里面,我们可以查看actions组所包含的所有函数。

    node.module里面所包含的文档指令,并不止这些,我们在这里就不逐一加以说明了。

 

    对于页面回调函数,我们应该注明它是页面回调,并给出函数的简单说明。

 

/**

 * Menu callback -- ask for confirmation of node deletion

 */

function node_delete_confirm($form, &$form_state, $node) {

  …

}


Drupal版本:

19 通过程序来检查你的代码

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

有两种主要的方式,可用来检查你的编码风格是否符合Drupal的编码标准:一种方式是使用一个Perl脚本,Drupal根目录下的scripts文件夹下面,有个名为code-style.pl脚本,它可用来检查你的Drupal代码;另一种方式是使用一个第3方模块coder

Coder模块的项目地址为http://drupal.org/project/coder,使用这个模块,我们便可以评估一个模块的代码。它有助于让我们熟悉Drupal的编码规范,提高我们的开发效率,帮我们节省不少时间。

    Coder模块的开发者提供了一个在线版本,http://upgrade.boombatower.com,通过在线提交你的模块,它就可以帮你完成代码的评估,Drupal6模块到Drupal7的初步升级。最初这个站点是免费的,现在改为了收费。

 

下载最新的coder版本,同时下载它依赖的grammar_parsergrammar_parser_liblibraries的最新开发版,将它们放到sites/all/modules/standard/下,接着在admin/modules页面启用CoderCoder ReviewCoder Upgrade模块,这和安装其它模块一样。

    现在导航到管理 » 配置 » 开发 » Coder”,点击“Run reviews”按钮,接着访问默认标签,我们就可以看到所有模块的代码评估了。以Field Validation模块为例,如图所示:

图片1.png 

    它会列出模块代码中的所有不符合规范的地方。

 

coder模块的scripts\coder_format文件夹下,提供了一个coder_format.php脚本。使用这个脚本,可以通过命令行的方式完成代码评估,这个脚本还会帮助我们修正代码格式错误。这个文件夹下面,同时还自带了coder_format.reg文件,方便在windows下面安装。


Drupal版本:

20 编写安全的代码

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

曾经有位学员打电话问我,说他的Drupal站点挂机了,他说他的站点可能受到了黑客攻击。问我能不能帮他查查,我告诉他,Drupal很稳定,通常不会出现这种情况。后来,原因出来了,原来是他网站所在服务器上面的另一套程序被人攻击了,导致了他的站点也受到了牵连。是的,这不是Drupal的错。我维护过很多Drupal站点,基本上没有见过被黑掉的Drupal站点。我前段时间,帮助我的一位大学老师维护他的个人站点,ASP的,小团队写的,里面挂满了木马,我不得不逐个文件夹进行核查。

是的,Drupal核心很安全了,因为Drupal核心程序得到了上百万个站点的检验了。但是这并不意味着说,你新上马的这个Drupal站点也是安全的。因为你的站点上面,安装了各种各样的第三方模块,以及自己定制的模块,这些代码与Drupal核心相比,存在安全漏洞的可能性更高一点。如果我们在编写自己的模块时,能够遵守代码安全相关的最佳实践,优先采用Drupal自身提供的API,我们就可以避免常见的安全漏洞。安全问题有很多,比较常见的有,跨站点脚本攻击(XSS)、CSRF、 SQL注入攻击、信息泄露、哈希长度扩展攻击。我们接下来介绍,Drupal提供了哪些技术,能够帮助我们避免这些安全漏洞。


Drupal版本:

21 跨站点脚本攻击(XSS)

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

跨站点脚本(XSS)指利用网站漏洞从用户那里恶意盗取信息。用户在浏览网站、使用即时通讯软件、甚至在阅读电子邮件时,通常会点击其中的链接。攻击者通过在链接中插入恶意代码,就能够盗取用户信息。攻击者通常会用十六进制(或其他编码方式)将链接编码,以免用户怀疑它的合法性。网站在接收到包含恶意代码的请求之后会产成一个包含恶意代码的页面,而这个页面看起来就像是那个网站应当生成的合法页面一样。许多流行的留言本和论坛程序允许用户发表包含HTMLjavascript的帖子。假设用户甲发表了一篇包含恶意脚本的帖子,那么用户乙在浏览这篇帖子时,恶意脚本就会执行,盗取用户乙的session信息。

能使用XSSHTML标签有<script>,<object>,<applet>,<embed>,<form>.用于XSS的常见语言有JavaScript,VBScript,HTML,Perl,C++,ActiveX,Flash.

 

现在的网站,都在强调与用户之间的互动,网站的很多内容,信息,是由普通用户生成的。对于大多数的用户来说,他们会按照我们的预期填写数据,然后提交给我们。但是也有很小的那么一部分,他们不按照规则形式。我们在方便前者的同时,不得不对后者做出防备。

 

假定你允许用户向你的网站输入HTML,期望他们这么输入

<em>大家好!</em> 此处省略10000...

 

但是他们输入了

<script src=”http://evil.example.com/xss.js”></script>

 

    当用户访问这个页面时,就有可能触发这个恶意的JS代码。对于受害者来说,会造成多种后果,可能会造成帐户信息被盗,访问各种虚假广告链,Cookie信息失窃等等。

    对于XSS,最简单的办法就是过滤用户输入,屏蔽用户输入的部分标签。我们来看看Drupal中,我们怎么解决这个问题。

 

 

Drupal提供了多种输入格式,用来过滤内容输出。以节点为例,用户创建了一篇文章,Drupal将文章正文原封不动的保存到数据库中,当生成web页面输出时,就会使用输入格式包含的过滤器对文本进行过滤,这样就可以很好的过滤恶意的HTML代码。为不同角色的用户,选用不同的输入格式。这是我们在构建一个站点时,从安全方面应该考虑的。

 

Drupal也提供了多个API函数,用来过滤用户的输入。我们在开发模块的时候,需要处理用户输入时,应该考虑使用这些函数,比如check_plaincheck_markup check_url filter_xssfilter_xss_admin。对于纯文本来说,我们使用check_plaincheck_url;对于富文本来说,我们使用check_markupfilter_xssfilter_xss_admin


Drupal版本:

22 使用check_plain()

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

check_plain做两件事情,一个是把HTML标签转义,二是检查字符串是不是UTF-8的。这两点都能帮我们防止XSS攻击。

 

我们举个例子,来看这段代码:

 

$output ="";

$user_input = '<script src=”http://evil.example.com/xss.js”></script>';

$output = check_plain($user_input);

return  $output;

 

用户的输入就是前面我们所给的一段恶意脚本,返回的输出已经经过了转义:

图片1.png 

我们看到此时的<script>标签已经是灰色的了,而不是蓝色的了。我们可以在页面的正文中看到对应的文本,此时的输入已经没有任何危害了。对应的页面内容如下所示:

图片2.png 

此时,如果我们把这个html页面保存起来,然后再用写字板打开对应的htm文件,我们会看到真实的输出:

<script src=”http://evil.example.com/xss.js”></script>

 

    此时我们已经把“<”作了转义,我们通过浏览器看到的则是用户的实际输入。

 

    如果我们的代码这样写:

$output ="";

$user_input = '<script src=”http://evil.example.com/xss.js”></script>';

//$output = check_plain($user_input);

$output = $user_input;

return  $output;

    我们通过firebug看到的输出则是:

图片3.png 

    对应的页面则没有内容输出。

 

   Drupal自带的一些函数,内置了对check_plain的支持,比如:

t():占位符比如'%name''@name'将被处理成为纯文本,如果想在这里使用html的,则使用'!name'

l():链接的标题,处理成为纯文本。如果想使用html的话,需要将html参数设置为TRUE

菜单项的标题,和面包屑的标题,自动使用check_plain

drupal_placeholder($text):占位符文本内置支持check_plain

区块描述内置支持check_plain

theme_username输出用户名时,内置支持check_plain

表单元素的#default_value属性自动支持check_plain

 

    如果Drupal已经内置支持了check_plain,我们就不需要重复使用它了。比如:

$form['good'] = array(
  '#type' => 'textfield',
  '#default_value' => $user_input,
); 

这些写就可以了。如果加上check_plain,就多余了。 

$form['bad'] = array(
  '#type' => 'textfield',
  '#default_value' => check_plain($user_input), //这里使用check_plain就多余了。
); 

 

    有些地方,则需要自己明确的调用check_plain。比如页面标题,区块标题,Watchdog消息,表单元素的#description#title属性、#value属性、#options属性。

比如:

drupal_set_title($user_input); // 存在安全隐患, drupal_set_title(check_plain($user_input));  // 正确。

//正确写法

$form['good'] = array(

  '#type' => 'textfield',

  '#title' => check_plain($user_input),

);

//错误写法

$form['bad'] = array(

  '#type' => 'textfield',

  '#title' => $user_input,

);


Drupal版本:

23 使用filter_xss()

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

函数filter_xss()的签名如下所示:

filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'code',

'ul', 'ol', 'li', 'dl', 'dt', 'dd'))

filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd'))

从这个函数的名字,我们就可以看出,它是专门用来防止XSS攻击的。在Drupal7中,这个函数是放在common.inc文件中的,在以前的Drupal6中则放在了filter.module文件中。

 

这个函数做四件事情:

1.删除可能欺骗浏览器的字符、结构体。

 

2. 确保所有HTML实体格式良好。

 

3. 确保HTML标签和标签属性的形式良好

 

4. 它确保没有HTML标签包含带有未允许协议的URL

 

5. 它确保所有的HTML标签,都不包含不允许的协议。比如javascript:。而允许的协议有'ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal'。你可以通过在settings.php中设置filter_allowed_protocols变量来修改这一列表。filter_xss()过滤不允许协议,是通过filter_xss_bad_protocol drupal_strip_dangerous_protocols函数实现的。

 


下面filter_xss()例子很多,modules/field/modules/list/list.module,文件中的list_field_formatter_view函数中,为了避免潜在的安全隐患,使用了field_filter_xss,代码如下

 

case 'list_key':

foreach ($items as $delta => $item) {

$element[$delta] = array('#markup' => field_filter_xss($item['value']));

}

break;

 

    这里的field_filter_xssfilter_xss作了简单的封装,设置了自己允许的标签:

function field_filter_xss($string) {

  return filter_xss($string, _field_filter_xss_allowed_tags());

}

 

对于与后台管理界面相关的内容过滤Drupal提供了函数filter_xss_admin()。它对filter_xss()做了简单的封装,只不过使用的可用标签更多一点,除了<script> 和 <style>标签以外,它包含了所有的可用标签。这是因为管理员比普通用户更加可信一点。使用它的一个例子是在主题中展示站点的宗旨(mission):

if (drupal_is_front_page()) {

$mission = filter_xss_admin(theme_get_setting('mission'));

}


Drupal版本:

24 使用check_url和valid_url

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

有时候我们需要处理用户提交的URL。我们首先需要检查用户提供的值是个有效的URL。此时我们可以使用函数check_url(),这个函数首先过滤掉恶意的协议,接着使用check_plain()来处理URL,代码如下

 

function check_url($uri) {

  return check_plain(drupal_strip_dangerous_protocols($uri));

}

此时由于我们使用了check_plain,所以返回的值不能用作drupal_attributes()里面的属性值。如果我们需要传递给drupal_attributes,那么可以直接调用drupal_strip_dangerous_protocols

如果我们只是想验证一下URL的有效性,此时可以使用valid_url()。它是按照RFC 3986标准来验证URL的。如果URL通过了测试,那么它返回TRUE,否则返回FALSE。需要注意的是,通过语法检查的URL也不一定安全。

如果你想输出一个URL时,可以使用函数url(),它在处理url时调用了drupal_encode_path(),从而实现对Drupal路径的编码。drupal_encode_path()PHPrawurlencode()函数做了封装。为了好看,没有对斜杠进行转义。

function drupal_encode_path($path) {

  return str_replace('%2F', '/', rawurlencode($path));

}

Drupal就是这样,很多时候不使用PHP自身的API函数,而是根据自己的需要,在上面做一些封装处理。这种情况很多,除了上面的drupal_encode_path,常见的还有drupal_substr,对substr做了封装,drupal_strlenstrlen做了封装。


Drupal版本:

25 SQL注入攻击

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

 SQL注入攻击是黑客对数据库进行攻击的常用手段之一。随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多。黑客通过把SQL命令插入到Web表单提交的查询字符串,最终达到欺骗服务器并执行恶意的SQL命令

/**

 * Implements hook_menu().

 */

function sql_injection_menu() {

  $items = array();

$items['sql/injection'] = array(

    'title' => 'SQL注入实例',

    'description' => 'SQL注入实例.',

    'page callback' => 'sql_injection_page_callback',

    'access callback' => TRUE,

  );

return $items;

}

 

/**

 * 页面回调函数.

 */

  $result = db_query("SELECT n.nid, n.title FROM

function sql_injection_page_callback(){

  //假定这个type是用户通过表单输入的,为了方便,这里直接赋值了。

  $type = 'page';

//$output ="123";

 {node} n WHERE n.type = '$type'");

  $items = array();

foreach($result as $record){   

  $items[] = l($record->title, "node/{$record->nid}");

}

return theme('item_list', array('items' => $items));

}

访问http://localhost/thinkindrupal/sql/injection,一切正常。我们得到想要的基本页面列表,如图所示。

图片1.png 

图 简单的page页面标题列表

 

 

 

    尽管这段程序也能正常工作,但是却存在安全隐患。将变量$type直接放到SQL中,这样用户就有可能发动SQL注入攻击。让我们将$type设置为:

$type = "page' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1 #";

    假定这就是用户的输入,此时我们将得到:

 

图片2.png 

图 在db_query()中不使用占位符引起的SQL注入

 

    看到什么了?像密码一样的信息,是的,我们把用户1的会话信息也显示了出来。出问题了把。当然,这危害还比较小,黑客有可能会使用SQL注入,获取更多的信息。比如注册用户的各种个人信息。

 

我们来对这段代码进行改进,其实使用Drupal7中的占位符,就可以避免SQL注入。代码如下:

$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = :type",array(':type' => $type));

如果用户输入的是:

$type = "page' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1 #";

此时结果为空。我们已经有效的防止了SQL注入攻击。

我们也可以采用动态查询的形式,代码如下所示:

$result = db_select('node', 'n')

          ->fields('n',array('nid','title'))

            ->condition('n.type',$type)  

            ->execute();

这也同样可以避免SQL注入攻击。


Drupal版本:

26 跨站点请求伪造(CSRF)

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

一个网站用户B可能正在浏览聊天论坛,而同时另一个用户A也在此论坛中,并且后者刚刚发布了一个具有B银行链接的图片消息。设想一下,A编写了一个在B的银行站点上进行取款的form提交的链接,并将此链接作为图片的src属性。如果B的银行在cookie中保存他的授权信息,并且此cookie没有过期,那么当B的浏览器尝试装载图片时,就会提交这个取款form和他的cookie,这样在没经B同意的情况下便授权了这次事务。

Drupal5.2以前,是存在CSRF安全漏洞,http://drupal.org/node/162360,在此以后,只要遵守Drupal表单API规范,便可以杜绝此类攻击。

 

Drupal表单API遵循HTTP/1.1规范,GET方法用于数据检索,除此以外不对数据作任何改动。如果需要对服务器进行改动,在Drupal中应该使用POST

 

其次,表单API带有令牌和标识ID,从而确保从POST请求中提交的表单数据来自于Drupal生成的表单。当你编写模块时,对于你的表单,一定要为其使用表单API,这样你就能够自动的获得保护。在你的模块中,对表单输入结果进行的任何操作,都应该放到该表单的提交函数中。

我们在实践中,需要避免两种情况:

1, 直接使用$_POST,并且使用html方式构建表单(绕过Drupal的表单API)。

2, 使用菜单回调直接处理修改数据的动作。

 

        对于前者,我们采用表单API既可,对于后者,我们也需要采用表单API,此时使用confirm_form(),在执行数据操作前,先确认一下。


Drupal版本:

27 哈希长度扩展攻击

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

哈希算法有很多,我们以MD5为例。首先MD5算法会对消息进行分组,每组64字节,不足64字节的部分用padding补齐。padding的规则是,在最末一个字节之后补充0x80,其余的部分填充为0x00padding最后的8字节用来表示需要哈希的消息长度。在对消息进行分组以及padding后,MD5算法开始依次对每组消息进行压缩,经过64轮数学变换。上一次压缩的结果,将作为下一次压缩的输入。

    长度扩展攻击的理论基础,就是将已知的压缩后的结果,直接拿过来作为新的压缩输入。在这个过程中,只需要上一次压缩后的结果,而不需要知道原来的消息内容是什么。

    为了避免潜在的安全隐患,在Drupal7中,我们不应该在代码中使用md5() sha1()函数,而应该使用更加安全的drupal_hash_base64()drupal_hmac_base64()

function drupal_hash_base64($data) {

  $hash = base64_encode(hash('sha256', $data, TRUE));

  // Modify the hash so it's safe to use in URLs.

  return strtr($hash, array('+' => '-', '/' => '_', '=' => ''));

}

    drupal_hash_base64使用了sha256算法,并使用base64进行编码,最后对哈希值作了字符替换,确保在URL中的安全性。


Drupal版本:

28 信息泄露

 作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

  信息泄露破坏了系统的保密性,它指信息被透漏给非授权的用户。这又分为多种情况。以前面sql注入的代码为例。

$result = db_select('node', 'n')

          ->fields('n',array('nid','title'))

            ->condition('n.type',$type)  

            ->execute();

    用户可以看到该类型下面的所有节点信息,当然这里面有些是需要做出限制的,比如未发表的节点,比如启用了节点访问控制模块后,用户无权访问的特定节点。此时我们需要对代码进行进一步的改进。

$result = db_select('node', 'n')

  ->fields('n',array('nid','title'))

  ->condition('n.type',$type)

          ->condition('n.status',1)

          ->addTag('node_access')    

  ->execute();

    这样用户便只能看到已发表的文章,同时节点访问控制模块也可以对我们的SQL进行修改,加上权限检查。


Drupal版本:

29 权限和页面回调

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com 

在我们编写模块时,需要注意菜单项中'access callback'access arguments’键的使用。在前面SQL注入的例子中,我们是这样的写的:

function sql_injection_menu() {

  $items = array();

$items['sql/injection'] = array(

    'title' => 'SQL注入实例',

    'description' => 'SQL注入实例.',

    'page callback' => 'sql_injection_page_callback',

    'access callback' => TRUE,

  );

return $items;

}

此时,任何人都可以访问这个页面。我们可以对它做出改进。比如我们可以这样设置

'access callback' => 'user_access',

'access arguments' => array('access content'),

这样只有具有“access content”访问内容)权限的用户才可以访问这个页面, “access content”是个很宽松的权限。我们也可以使用hook_permission()来定义你自己的权限,并使用它们来保护你的菜单回调函数。当然我们也可以自己定义一个访问控制回调函数,将其设置为'access callback'。有关这方面的更多信息,可参看菜单系统一章。


Drupal版本:

30 文件安全

 作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

在以前,如果你把Drupal站点安装在了IIS上,由于Drupal没有提供IIS下面的配置文件,假定你又不熟悉IIS的配置的话,那么有可能其它用户能将你的module文件下载到本地。通过查看页面源代码,就能看到JS的路径信息,如果JS没有压缩,就可以推测出来module文件的所在,直接访问.module文件,便可以将其下载到本地。

Drupal自带的.htaccess文件包含了以下代码,可以保护后台程序文件不被他人下载:

# Protect files and directories from prying eyes.

<FilesMatch "\.(engine|inc|info|install|module|profile|po

|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(code-style\.pl

|Entries.*|Repository|Root|Tag|Template)$">

Order allow,deny

</FilesMatch>

 

Order指令设为了allow,deny,这意味着默认的行为是拒绝。对于匹配上述规则的文件的请求,都会被拒绝访问。如果直接我们访问:http://localhost/thinkindrupal/sites/all/modules/custom/sql_injection/sql_injection.module。此时会得到一个拒绝访问页面。

图片1.png 

 

如果我们将“Order allow,deny”修改为“Order deny,allow”,此时再访问这个页面,我们竟可以下载这个文件了。

图片2.png 

 

对于IIS服务器,Drupal7自带的web.config里面也包含和.htaccess对应的规则:

<rule name="Protect files and directories from prying eyes" stopProcessing="true">

<match url="\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(\..*|Entries.*|Repository|Root|Tag|Template)$" />

<action type="CustomResponse" statusCode="403" subStatusCode="0" statusReason="Forbidden" statusDescription="Access is forbidden." />

</rule>


 


Drupal版本:

31 维护一个Drupal模块

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

如果你创建了一个Drupal模块,想把它贡献到drupal.org的话,你首先需要申请一个账号,这个账号可以向GIT提交代码,这里是Drupal项目的Git,也就是以前的CSV账号。通常情况下,这个申请都是比较困难的,因为需要得到GIT管理员的批准,他们会对代码里面的问题提出很多的意见,让你修改,你可以按照他们的要求,来修改,直到账号批准下来。对于很多中文开发者来说,这个过程比较麻烦。还有一个办法就是,找一个拥有GIT账号的人,帮你创建这个项目,然后把代码提交上来,这个时候你可以申请成为项目的维护者,当你成为了一个项目的维护者的时候,自然就拥有了代码提交的权限。我就是通过后一种途径获得的CSV账号,现在升级成为GIT账号了。当然,你还可以创建一个沙盒项目,通过这种方式维护你的代码,但是沙盒项目里面有很多的限制。

当我们创建一个新的项目的时候,需要看看,是不是已经有了这样的项目,如果已经有了这样的项目,最好的办法是去完善已有的项目,而不是做重复的工作。我们这里以要编写的面包屑模块为例,其实已经有很多面包屑模块,这个时候,如果我们有新的想法,此时可以申请成为已有模块的维护者,但是如果没有人批准的话,我们再去创建一个啊。比如现在已经有了一个breadcrumb模块了,如果我们再去创建一个entity_breadcrumb或者breadcrumb2的话,drupal.org上面,就会有人冒出来,提出这样的问题,这个模块与什么什么模块有哪些区别啊? 如果没有区别的话,合并了算了。我在breadcrumb模块的问题列表里面申请成为维护者,但是没有回应,这个时候,我就可以创建一个新的模块了,再有人来问,也有了理由答复他们。

我们把要创建的模块命名为breadcrumb2,现在我们来添加一个这样的项目,如果你已经拥有GIT账号了,此时可以访问http://drupal.org/project/user,这里有你已有的项目列表,我维护的模块有点多了,在这里的截图太大了。

图片1.png 

点击最下面的“Add a new project”链接,进入页面http://drupal.org/node/add/project-project,这个页面就是一个普通的节点添加页面,我们在这里输入相关的信息即可:

图片2.png 

这是创建好的样子:

图片3.png 

项目描述里面,咱用的可是英文啊,不过这些英文都是从profile2模块里面复制过来的,改了一下里面的名字而已。中间的对号的右边是提示信息,提示我们Git的资源库正在准备创建中,只需要等待几秒钟就可以了。

现在,我们点击页面标签里面的“Version control”标签,进入页面http://drupal.org/project/breadcrumb2/git-instructions。这个时候,我们的Git资源库还没有建立起来,我们会看到以下信息:

图片4.png 

这里面列出来了Git命令,这些命令对于像我这样不熟悉Git的用户,就很方便了。我用的是Windows下面的Git客户端,大家可以到http://code.google.com/p/msysgit/下载。安装好以后,会有一个Git Bash快捷键,点击这个快捷键,就会出来Git的命令行式的对话框。

图片5.png 

我把维护的项目都放到E盘的ghr/test目录了,需要使用命令先导航到这个目录。输入以下命令即可:

cd e:/ghr/test

接下来,我们按照页面上面的提示,依次执行相应的命令即可。这里分别是:

mkdir breadcrumb2

 cd breadcrumb2

 git init

 echo "name = Breadcrumb2" > breadcrumb2.info

 git add breadcrumb2.info

 git commit -m "Initial commit."

 git remote add origin g089h515r806@git.drupal.org:project/breadcrumb2.git

 git push origin master

有些命令,比如mkdirgit add breadcrumb2.info,我们也可以通过Window的图形界面完成,能够通过图形界面完成的,我都采用图形界面。省事。

当输入最后一个命令“git push origin master”时,会提示输入你在git.drupal.org上面的密码,输入密码后,才会将本地的代码提交上去。

执行完这些命令以后,刷新,此时这个页面的内容会有变化,此时显示出来更多的Git命令:

图片6.png 

这些命令上面的提示都是英文的,我们这里分别介绍一下。

 

1,如果是第一次下载这些资源库里面的内容的话,也就是将Git 上面的代码复制到本地,使用以下命令:

git clone --recursive --branch master g089h515r806@git.drupal.org:project/breadcrumb2.git

 cd breadcrumb2

这里的master是当前的分支,我们一般是不使用这个分支的,而是使用7.x-1.x7.x-2.x8.x-1.x这样的分支。在下面我们会介绍如何创建一个新的分支。如果本地已经有了现成的代码,就不需要使用这段命令了。

 

2,将你本地的资源库,与你的drupal.org帐户关联起来,这里的关联使用的是电子邮件。命令如下:

git config user.email g089h515r806@gmail.com

当然,这里的电子邮件地址不一定使用真实的,也可以使用这样的形式g089h515r806@174740.no-reply.drupal.org。能够在Git上面将你标识出来即可。

 

3,检查资源库的状态,用来检查当前的代码与Git上面的代码是否同步:

git status

 

4,切换到一个不同的分支:

git branch -a

git checkout [branchname]

这里的第一行命令,用来列出所有可用的分支,也就是你拥有权限访问的分支,第二行命令用来切换。

 

5,提交本地的变更:

git add -A

git commit -m "Issue #[issue number] by [comma-separated usernames]: [Short summary of the change]."

如果这个变更是由别人提交的补丁,此时最好注明作者,也就注明谁做的贡献,我们可以在说明里面列出来,也可以使用下面的命令:

git commit --author="deviantintegral <deviantintegral@71291.no-reply.drupal.org>" -m "Issue #[issue number] by [comma-separated usernames]: [Short summary of the change]."

这里面的作者选项,可以在作者所在的页面http://drupal.org/user/71291,里面的“Git attribution”找到。deviantintegralField Validation模块提交过补丁,我在将代码提交上去的时候,将这个提交的作者设置为了deviantintegral。这样可以调动更多地人的积极性。

 

6,将你本地的修改,提交到Drupal.org上面的资源库中:

git push -u origin master

这个命令通常和上面的两个命令一起用的。

 

7,更新最新的代码,使本地的代码与Drupal.org上面的资源库保持同步:

git pull origin master

这里是以线上的代码为标准的,本地的和线上的保持一致。通常在为模块创建补丁的时候,使用这个命令,来做准备工作。

 

8,创建一个补丁文件:

git diff >  [description]-[issue-number]-[comment-number].patch

中括号里面的文字需要替换为你自己的。

 

9,应用一个补丁,你需要把别人的补丁下载到本地的对应目录,然后执行以下命令:

git apply -v [patchname.patch]

 

打好补丁以后,需要将这个补丁删除:

rm  [patchname.patch]

 

10,撤销没有提交的变更,首先将变更保存到一个文件里面:

git checkout [filename]

然后,撤销所有的变更:

git reset –hard

 

11,创建一个新的开发分支,并将其传到drupal.org上:

git checkout -b 7.x-1.x

git push -u origin 7.x-1.x

12,为稳定版打标签:

git checkout  7.x-1.x

git tag 7.x-1.0

git push origin 7.x-1.0

如果我们当前已经处在了7.x-1.x,此时就不需要执行第一行命令了。除了7.x-1.0以外,还可以用7.x-1.0-alpha1, 7.x-1.0-beta1这样的名字。

 

当我们创建一个新的项目后,记住,不要使用master分支,而是创建一个专门的分支,这里我们为Breadcrumb2执行命令11里面所列的命令。平时维护项目的时候,用到哪些命令,现查现用即可。另外需要注意的是,访问这个页面的时候,上面有个下拉选择框:

图片7.png 

这里可以选择master7.x-1.x7.x-2.x这样的开发分支,选择后,下面的命令提示也会有相应的变化。

 

如果我们编写的模块,对很多人有用,慢慢地,用的人就会多起来,如果模块的代码质量不好,功能有限,很快就会无人问津了。模块本身是否成功,很大程度上,取决于模块的维护者。


Drupal版本:

5 流程控制语句

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

流程控制语句是程序中用来控制执行流程的指令,比如条件语句和循环语句。条件语句有ifelse elseif、和switch语句。循环语句有whiledo-whileforforeach

 

流程控制语句在关键字(ifelseif、 whilefor, 等等)和开括号之间应有一个空格,从而将其与函数调用(也使用圆括号,但是没有空格)区分开来。“{”应该与关键字位于同一行(而不是自成一行),这一点对于很多熟悉PHP的程序员来说,开始的时候可能很不适应,但这就是Drupal的规范,它与PHP的规范还是有区别的。“}”应该自成一行。

    下面的这段代码摘自node.modulenode_block_list_alter函数:

 

if (!empty($node)) {

// This is a node or node edit page.

if (!isset($block_node_types[$block->module][$block->delta][$node->type])) {

// This block should not be displayed for this node type.

unset($blocks[$key]);

continue;

}

}

elseif (isset($node_add_arg) && isset($node_types[$node_add_arg])) {

// This is a node creation page

if (!isset($block_node_types[$block->module][$block->delta][$node_add_arg])) {

// This block should not be displayed for this node type.

unset($blocks[$key]);

continue;

}

}

else {

// This is not a node page, remove the block.

unset($blocks[$key]);

continue;

}

花括号“{}”一般总是使用的,即便是不需要的时候,为了增强代码的可读性,并降低出错的可能性,也应使用“{}”。下面的正确代码摘自于代码摘自node.modulenode_mark函数。

 

错误的

if (!$user->uid)

  return MARK_READ;

正确的

if (!$user->uid) {

return MARK_READ;

}

切换语句的格式应该这样(注意break;”语句在默认情况下不是必需的),摘自于代码摘自node.modulenode_block_view函数

switch ($delta) {

case 'syndicate':

$block['subject'] = t('Syndicate');

$block['content'] = theme('feed_icon', array('url' => 'rss.xml', 'title' => t('Syndicate')));

break;

 

case 'recent':

if (user_access('access content')) {

$block['subject'] = t('Recent content');

if ($nodes = node_get_recent(variable_get('node_recent_block_count', 10))) {

$block['content'] = theme('node_recent_block', array(

'nodes' => $nodes,

));

} else {

$block['content'] = t('No content available.');

}

}

break;

}

当一个情况执行完以后,打算继续执行下一情况时,此时可以省略“break;”语句。


Drupal版本:

6 函数调用

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

在函数调用中, 在操作符(=, <, >,等等)的两边应该各有一个空格,而在函数名和函数的开括号之间则没有空格。在函数的开括号和它的第一个参数之间也没有空格。中间的函数参数使用逗号和空格分隔,在最后一个参数和闭括号之间没有空格。在上面的例子中,区块内容的赋值语句就说明了这几点:

正确的

$block['content'] = theme('feed_icon', array('url' => 'rss.xml', 'title' => t('Syndicate')));

 

如果这样写就错误了

$block['content']=theme ('feed_icon',array('url'=>'rss.xml','title'=>t('Syndicate')));

 

 

这个规则也存在例外的情况。在一个包含多个相关赋值语句的代码块中,如果能够提高可读性,那么可以在赋值操作符周围插入多个空格(摘自node.moduletemplate_preprocess_node函数):

 

$variables['date']      = format_date($node->created);

$variables['name']      = theme('username', array('account' => $node));

 

$uri = entity_uri('node', $node);

$variables['node_url']  = url($uri['path'], $uri['options']);

$variables['title']     = check_plain($node->title);


Drupal版本:

7 函数声明

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

在函数的名字和它的开括号之间没有空格。在编写函数时,如果它的有些参数需要使用默认值,那么需要把这些参数列在后面。还有,如果你的函数生成了任何有用的数据,那么你需要返回该数据,以供调用者使用。下面给出了一些函数声明的例子摘自node.module

 

错误的

function node_is_page ($node) {

  $page_node = menu_get_object();

  $value = (!empty($page_node) ? $page_node->nid == $node->nid : FALSE);

}

 

function node_title_list ($title = NULL, $result) {

}

 

正确的

function node_is_page($node) {

  $page_node = menu_get_object();

  return (!empty($page_node) ? $page_node->nid == $node->nid : FALSE);

}

 

function node_title_list($result, $title = NULL) {

  …

}


Drupal版本:

8 函数名字

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

Drupal中,函数的名字都是小写的,为了避免命名空间冲突,通常以模块的名字或者它们所属系统的名字作为前缀。下划线用来分隔函数名字的描述性部分。在模块名的后面,通常应该紧跟一个动词,接着是动词作用的对象:modulename_verb_object()。在下面的第一个例子中,函数名字没有正确的使用模块前缀,并且动词和它的对象颠倒了。在接下来的例子中,很明显,修正了这些错误。

 

错误的

function save_node_action ($node) {

...

}

 

正确的

function node_save_action ($node) {

...

}

私有函数与其它函数一样,遵守相同的习惯,不过它在函数名字前面加了一个下划线。

function _node_index_node($node) {

  …

}


Drupal版本:

9 数组

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

对于数组,也是使用空格对它的每个元素和每个赋值操作符进行分隔的。如果数组区块跨越了80个字符,那么每个元素都应独立成行。为了提高可读性和可维护性,最好将每个元素全部独立成行。这样你就可以方便的添加、删除数组元素。

 

错误的

function node_uri($node) {

  return array('path' => 'node/' . $node->nid);

}

正确的

function node_uri($node) {

  return array(

    'path' => 'node/' . $node->nid,

  );

}

注意 数组中最后一个元素的后面有一个逗号,这不是一个PHP错误。最后的这个逗号,在PHP中是可有可不有的。这里加了逗号,是为了避免犯错,如果最后没有逗号,假如开发者在数组列表的最后又添加了一个元素,此时就会引起语法错误。这个规范是推荐使用的,不过它不是必须的。

 

在创建内部的Drupal数组时,比如菜单项或者表单定义,总是将每个元素单独成行:

 

$items['node/add'] = array(

'title' => 'Add content',

'page callback' => 'node_add_page',

'access callback' => '_node_add_access',

'file' => 'node.pages.inc',

);


Drupal版本:

1编码规范

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

Drupal社区认为,它的核心代码必须遵守一个标准的规范,从而提高代码的可读性,也使得初学者更容易学习DrupalDrupal官方也鼓励第3方模块的开发者采用这些规范。实际上,如果你没有遵守编码规范,你的模块仍然能够正常工作,但是模块中的代码,不利于同行之间的交流。我们首先学习一下具体的规范,接着介绍了一些工具,用来自动检查代码的质量(甚至可以为你纠正代码)。


Drupal版本:

2行缩进

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.

Drupal代码的缩进,使用的是两个空格,而不是tab键。如果你习惯了使用tab键,那么你可以在你的编辑器中,设置一个首选项,将tab键设置为两个空格,这样你就可以继续使用Tab键,并且也能达到缩进两个空格的目的。


Drupal版本:

3运算符

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

对于所有的二元运算符,比如+-=!===>,应该在运算符的前面和后面增加一个空格,从而增加可读性。例如:

正确:

     $arg[0] == 'node' && $arg[1] == 'add' && $arg[2]

错误:

      $arg[0]=='node'&&$arg[1]=='add'&&$arg[2]

     对于一元运算符,则不应该包含空格。比如:

$i++;

     对于三元运算符,符号两边应该有空格:

return is_object($node) ? $node->type : $node;


Drupal版本:

4PHP开始和结束标签

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

包含代码的文件,比如.module.inc文件,会使用PHP代码开始标签,如下所示:

 

<?php

...

 

Drupal中不能使用开始标签的简写形式“<?”

结束标签“?>”,在PHP中不是必须的,但是在Drupal的模块代码中,它是被禁止使用的。如果使用了PHP的结束标签,那么可能会带来一些潜在的问题。不过这也有例外情况,在Drupal的模板文件中,为了退出PHP并且回到HTML中,此时会使用结束标签,例如,在themes/bartik/templates/node.tpl.php中,就同时用到了PHP的开始、结束标签:

 

  <?php print render($title_prefix); ?>

  <?php if (!$page): ?>

    <h2<?php print $title_attributes; ?>>

      <a href="<?php print $node_url; ?>"><?php print $title; ?></a>

    </h2>

  <?php endif; ?>

  <?php print render($title_suffix); ?>


Drupal版本: