分析 WordPress `current_user_can()` 函数的源码:如何通过 `map_meta_cap` 钩子实现细粒度权限控制。

咳咳,各位观众老爷们,晚上好!今天咱们聊点WordPress里面有点意思的东西,就是current_user_can()这个函数,以及它背后的一个大功臣——map_meta_cap钩子。

想象一下,你开了一家面包店,你想让不同的人干不同的活儿。老板娘可以啥都干,收银员只能收钱,面包师傅只能烤面包。这权限控制,在WordPress里,current_user_can()就是负责问:“这个人能不能干这个事儿?”的。而map_meta_cap,就是那个帮你细化规则,让权限控制更精准的幕后高手。

一、current_user_can():权限判断的门卫

先来看看current_user_can()是干嘛的。这函数接受一个或多个参数:

  • 第一个参数: 你要判断的“能力”(capability),比如edit_posts(编辑文章),delete_pages(删除页面)等等。
  • 后面的参数: 可选参数,通常是你要操作的对象的ID,比如文章ID,页面ID。这玩意儿很重要,因为权限判断有时候需要根据对象来决定。
<?php
if ( current_user_can( 'edit_post', 123 ) ) {
    echo '用户有权限编辑ID为123的文章';
} else {
    echo '用户没有权限编辑ID为123的文章';
}
?>

这段代码的意思是:如果当前用户有编辑ID为123的文章的权限,就显示“用户有权限编辑ID为123的文章”,否则显示“用户没有权限编辑ID为123的文章”。

二、Capability:能力的标签

Capability,咱们可以把它理解为“能力的标签”。WordPress自带了很多capability,比如:

Capability 描述
read 阅读
edit_posts 编辑文章
publish_posts 发布文章
delete_posts 删除文章
edit_pages 编辑页面
delete_pages 删除页面
manage_options 管理选项(通常只有管理员才有)
upload_files 上传文件
moderate_comments 审核评论

这些capability都是比较笼统的。比如edit_posts,它指的是“编辑文章”的权限,但它没有区分是“编辑自己的文章”还是“编辑别人的文章”。 这时候,map_meta_cap就派上用场了。

三、map_meta_cap:权限映射的魔术师

map_meta_cap是一个过滤器钩子。它的作用是:把一些“抽象的”capability(也就是所谓的“meta capability”)映射到具体的capability上。

Meta Capability vs. Primitive Capability

  • Meta Capability: 抽象的,需要根据上下文才能确定的capability。比如edit_post(编辑文章),delete_post(删除文章)。
  • Primitive Capability: 具体的,可以直接分配给角色的capability。比如edit_others_posts(编辑别人的文章),delete_published_posts(删除已发布的文章)。

map_meta_cap的作用,就是把像edit_post这样的meta capability,根据文章的作者、状态等信息,映射到像edit_others_postsedit_published_posts这样的primitive capability上。

工作原理

current_user_can()被调用时,如果传入的capability是一个meta capability,那么WordPress会触发map_meta_cap钩子。所有注册到这个钩子上的函数都会被执行,它们负责根据传入的参数(用户ID、capability、对象ID等),来决定应该返回哪些具体的capability。

举个栗子:edit_post的映射

假设我们要判断用户是否有编辑ID为123的文章的权限:

<?php
if ( current_user_can( 'edit_post', 123 ) ) {
    echo '用户有权限编辑ID为123的文章';
} else {
    echo '用户没有权限编辑ID为123的文章';
}
?>

在这个例子中,edit_post就是一个meta capability。当current_user_can()被调用时,WordPress会触发map_meta_cap钩子。WordPress内部已经有一个默认的函数注册到了map_meta_cap钩子上,它会根据以下规则进行映射:

  1. 如果用户是管理员, 那么直接返回do_not_allow(表示不允许),因为管理员默认拥有所有权限,不需要进行额外的判断。(这里的设计有点反直觉,返回do_not_allow表示“不需要进一步检查,允许通过”,而不是“不允许通过”。)
  2. 如果文章的作者是当前用户, 那么会检查用户是否拥有edit_posts的权限。如果有,则返回edit_posts
  3. 如果文章的作者不是当前用户, 那么会检查用户是否拥有edit_others_posts的权限。如果有,则返回edit_others_posts
  4. 如果文章是已发布的, 那么还会检查用户是否拥有edit_published_posts的权限。

通过这些映射,edit_post这个meta capability就被转化成了具体的primitive capability,current_user_can()就可以根据这些primitive capability来判断用户是否有权限编辑文章了。

四、自定义map_meta_cap:打造你的专属权限系统

WordPress默认的map_meta_cap已经很强大了,但有时候我们还需要自定义权限规则。比如,我们想实现以下功能:

  • 只有“编辑”角色的用户才能编辑某个特定分类的文章。

要实现这个功能,我们需要自定义一个函数,注册到map_meta_cap钩子上。

<?php
/**
 * 自定义权限映射函数
 *
 * @param array   $caps    Required primitive capabilities for the requested action.
 * @param string  $cap     Capability being checked.
 * @param int     $user_id User ID.
 * @param array   $args    Adds the context to the cap. Typically the object ID.
 * @return array Actual capabilities for performing the action.
 */
function my_custom_map_meta_cap( $caps, $cap, $user_id, $args ) {

    // 检查是否是编辑文章的请求
    if ( 'edit_post' === $cap ) {

        // 获取文章ID
        $post_id = $args[0] ?? 0; // 使用空合并运算符,避免未定义索引错误

        if ( ! empty( $post_id ) ) {
            // 获取文章对象
            $post = get_post( $post_id );

            if ( $post ) {
                // 获取文章的分类ID
                $categories = wp_get_post_categories( $post_id );

                // 检查文章是否属于特定分类(这里假设分类ID为10)
                if ( in_array( 10, $categories, true ) ) {

                    // 获取用户对象
                    $user = get_user_by( 'id', $user_id );

                    // 检查用户是否是“编辑”角色
                    if ( in_array( 'editor', (array) $user->roles, true ) ) {
                        // 如果是,则允许编辑
                        return array( 'edit_posts' ); // 返回 edit_posts 而不是 do_not_allow,因为我们只是限制了特定分类的文章
                    } else {
                        // 如果不是,则拒绝编辑
                        return array( 'do_not_allow' ); //明确拒绝,防止其他capability允许
                    }
                }
            } else {
                // 如果文章不存在,拒绝编辑
                return array( 'do_not_allow' );
            }
        } else {
            // 如果文章ID为空,拒绝编辑
            return array( 'do_not_allow' );
        }
    }

    // 如果不是编辑文章的请求,则返回原始的capabilities
    return $caps;
}

// 注册钩子
add_filter( 'map_meta_cap', 'my_custom_map_meta_cap', 10, 4 );
?>

这段代码做了以下几件事:

  1. 定义了一个函数my_custom_map_meta_cap 这个函数接收四个参数:
    • $caps:原始的capabilities数组。
    • $cap:要检查的capability(比如edit_post)。
    • $user_id:用户ID。
    • $args:传递给current_user_can()的额外参数(比如文章ID)。
  2. 检查$cap是否是edit_post 如果不是,则直接返回原始的$caps,不做任何修改。
  3. 如果$capedit_post 则获取文章ID,然后获取文章对象。
  4. 获取文章的分类ID, 然后检查文章是否属于特定分类(这里假设分类ID为10)。
  5. 如果文章属于特定分类, 则获取用户对象,然后检查用户是否是“编辑”角色。
  6. 如果是“编辑”角色, 则返回array( 'edit_posts' ),表示允许编辑。
  7. 如果不是“编辑”角色, 则返回array( 'do_not_allow' ),表示拒绝编辑。
  8. 使用add_filter()函数,my_custom_map_meta_cap函数注册到map_meta_cap钩子上。

代码解释

  • $args[0]current_user_can()的第二个参数(文章ID)会作为$args数组的第一个元素传递给map_meta_cap钩子。
  • get_post( $post_id ):根据文章ID获取文章对象。
  • wp_get_post_categories( $post_id ):根据文章ID获取文章的分类ID数组。
  • in_array( 10, $categories, true ):检查分类ID数组中是否包含ID为10的分类。true表示进行严格类型比较。
  • get_user_by( 'id', $user_id ):根据用户ID获取用户对象。
  • (array) $user->roles:将用户角色属性转换为数组。
  • in_array( 'editor', (array) $user->roles, true ):检查用户角色数组中是否包含“editor”角色。

注意事项

  • map_meta_cap钩子非常强大,但也需要谨慎使用。不正确的映射可能会导致权限混乱,甚至安全问题。
  • 在自定义map_meta_cap函数时,一定要考虑各种情况,确保你的权限规则是正确的、完整的。
  • 尽量避免在map_meta_cap函数中执行复杂的数据库查询或其他耗时操作,因为这会影响网站的性能。
  • 确保你的代码具有良好的可读性和可维护性,方便以后进行修改和调试。
  • 返回 do_not_allow 时要谨慎,确保没有其他capability会覆盖你的拒绝。
  • 在 WordPress 5.9 及更高版本中,map_meta_cap 钩子还可以传递 $object_id 参数,如果你只需要对象 ID,可以使用 $args[0] 或者直接使用 $object_id

五、更复杂的栗子:基于文章状态的权限控制

假设我们想实现以下功能:

  • 只有“管理员”才能删除已发布的文章。
  • “编辑”可以删除草稿文章或者自己发布的文章。
<?php
/**
 * 自定义权限映射函数
 *
 * @param array   $caps    Required primitive capabilities for the requested action.
 * @param string  $cap     Capability being checked.
 * @param int     $user_id User ID.
 * @param array   $args    Adds the context to the cap. Typically the object ID.
 * @return array Actual capabilities for performing the action.
 */
function my_custom_delete_post_cap( $caps, $cap, $user_id, $args ) {

    if ( 'delete_post' === $cap ) {
        $post_id = $args[0] ?? 0;

        if ( ! empty( $post_id ) ) {
            $post = get_post( $post_id );

            if ( $post ) {
                $user = get_user_by( 'id', $user_id );

                if ( 'publish' === $post->post_status ) {
                    // 已发布的文章
                    if ( in_array( 'administrator', (array) $user->roles, true ) ) {
                        // 只有管理员才能删除
                        return array( 'delete_published_posts' ); // 使用已有的capability
                    } else {
                        return array( 'do_not_allow' );
                    }
                } else {
                    // 草稿或其他状态的文章
                    if ( in_array( 'editor', (array) $user->roles, true ) || $post->post_author == $user_id ) {
                        // 编辑可以删除,或者作者自己可以删除
                        return array( 'delete_posts' ); // 使用已有的capability
                    } else {
                        return array( 'do_not_allow' );
                    }
                }
            } else {
                return array( 'do_not_allow' );
            }
        } else {
            return array( 'do_not_allow' );
        }
    }

    return $caps;
}

add_filter( 'map_meta_cap', 'my_custom_delete_post_cap', 10, 4 );
?>

这个例子更复杂一些,它根据文章的状态和用户的角色,来决定是否允许删除文章。

六、总结

current_user_can()map_meta_cap 是 WordPress 权限控制的核心。current_user_can() 负责发起权限检查,map_meta_cap 负责将抽象的权限映射到具体的权限。通过自定义 map_meta_cap 钩子,我们可以实现各种复杂的权限控制需求,打造一个安全、灵活的网站。

记住,权限控制是一个需要仔细考虑的问题。在实现自定义权限规则时,一定要充分测试,确保你的规则是正确的、完整的,并且不会对网站的性能产生负面影响。

好了,今天的讲座就到这里。希望大家有所收获!如果有什么问题,欢迎提问。 散会!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注