首页 > 代码库 > [自己动手改wordpress.3]给wordpress加上禁止附件外链

[自己动手改wordpress.3]给wordpress加上禁止附件外链

前言, 

         有几个月没写php了. 刚好公司有个web的项目需要改下之前他们搭建的Wordpress的站点, 因为给所有的page/post都加上了权限组来控制权限和SSO登录认证一体化. 但是附件却没有保护到. Wordpress直接暴露了附件的服务器上的地址.作为一个安全隐患, 这个需要修复.

本文适合有一些WP的插件开发经验的朋友.


1,数据准备,

 WP的附件attachment存放在Wordpress的后台的Media这个页面来控制,也可以直接通过发page的时候添加媒体文件来实现.

数据库存放在wp_posts 和wp_post_meta, 字段是`post_type`=‘attachment‘

也就是每当你需要发一篇文章的时候, 当你点击了一次上传媒体文件, Wordpress会存一个post到wp_posts表和wp_post_meta表. 一个文件对应一次数据的插入.

 其中wp_post_meta表有4个字段, 分别是meta_id, post_id, meta_key, meta_value

    meta_key=‘_wp_attached_file‘ 的数据就是我们的媒体文件了, 对应这条数据的meta_value是一个包含了上传时间和文件名的字符串.

技术分享


  而在wp_posts表, 对应的post_id的就是这个媒体文件的post的数据, 这里面我们最主要关心的是post_type字段和post_mine_type字段. 这里我上传的是一个mp3文件, 所以

对应的数据如下.

技术分享


2, 思路拓展

 要改变文章里面的数据, 就有几种方案. 通过上面的数据, 可以看见Wordpress是把一个相对或者绝对的路径存到了数据库. 这个地址就暴露在了外面. 在发文章的时候, Wordpress同样也存入了这个地址.  

假定我们的域名是cnwp.com, 现在想实现的是所有的附件链接都最终指向cnwp.com/download.php?fdi=xxxx来控制文件下载.

这导致了如果你只修改了media的相关数据, 页面page里面存入的链接是已经保存下来的, 没有被改掉. 

通过阅读源码, 发现Wordpress的insert post在插入post数据的时候, 如果判断到有attachment附件的数据插入, 就会提前去调wp_insert_attachment的函数. 所以这里可以使用apply_filter来过滤insert的数据. 但是我们是在插件里面来调, 钩子在获取数据的时候数据已经提前插入了. Wordpress没有提供修改插入附件的filter给我们使用. 这样修改了原始的数据, 实现是比较麻烦和复杂. 这条路比较难. 现卡在这里. 于是又去阅读了一下Wordpress其他的下载管理的插件, 做的比较好的是WPDM, download manager这个插件, 但是这个插件最终也是暴露了实际的下载地址. 它只是把按照日期目录排的文件移到了它自定的一个位置. 实际是一样的. 还是收费的. 而且对于之前的权限组不能控制到这个链接. 只控制到了post的content的数据. 所以也放弃.

查阅到了the_content和wp_get_attachment_url 两个filter, 一个是在控制page的content内容, 一个是控制attachment附件页面的url. 

前面说过, WP每次在上传附件的时候, 实际上也发了一条post的数据, 那么既然是post的数据, 就有一个地址, 这个地址就是 http://cnwp.com/xxx/?attachment_id=103 类似的地址,xxx是network sites(WP的多站点子目录模式, 还有多站点子域名模式, 类似xxx.cnwp.com), 这个地址就是attachment附件的页面. 


所以我们要改的就是改掉页面上所有attachment的链接, 不管它在一个页面里面是以http://cnwp.com/xxx/?attachment_id=xxx的形式出现, 

还是以http://cnwp.com/xxx/wp-content/uploads/sites/6/2015/01/Yanni-Ninghtingale.mp3  这样的链接出现.

我们要保证后一个url的实际的文件地址,不会出现在用户的面前.


3,  首先去插件页面, 加filter, 

技术分享

array($sc, ‘scAttachmentRewrite‘) 这个是调的我的一个类, 里面有一个scAttachmentRewrite的函数, 后面的10是优先级, 1是the_content会传入进scAttachmentRewrite函数的参数

同理,scAttachmentUrlRewrite也是类似的一个.

再来看下函数. 这个函数是来重写the_content的url的, 通过正则来分段匹配出数据. 然后获取到页面的post的id, 这个post_id是和相关的权限组作了绑定, 这里我取出来是为了方便后面处理. 通过scUpdateAttachUrl函数来更新或者插入一个自定义的关联表, 里面存的是一会儿要重写的url的id, 这样就可以通过这个id来作新的替换掉的id的值


    /**
     * Rewrite the content attachment links.
     *
     * @param string $content theContent.
     *
     * @return mix.
     */
    public function scAttachmentRewrite($content)
    {
        $pattern = "/<a href=http://www.mamicode.com/"(/S+)(/wp-content/uploads/sites/)([0-9]+)(/S+)/">/";>

scAttachmentUrlRewrite处理的流程和下面的函数基本类似. 唯一不同的是因为是单个附件的page, 所以需要处理掉图片,音频等直接呈现在用户面前的url, 保证它们不会被重写掉. 所以就需要用获取到的post_id和这个attachment的页面的数据去查询post表里面post_mime_type=attachment的数据,排除掉media,png,gif等的数据.保证只重写对应格式的.

其中的代码片段, 其他和上面一样, 不过wp_get_attachment_url传入的参数是一个url地址, 正则就稍微不一样一点儿,

        $pattern = "/(\S+)(\/wp-content\/uploads\/sites\/)([0-9]+)(\S+)/";

 scQueryMediaMeta就是去查询的对应的post数据. 进而用post_mime_type来对比. 如果有其他格式的, 大家也可以自己加对应的

                $checkMedia = $this->scQueryMediaMeta($match[3], $match[4], $post_id);
                if (!$checkMedia) {
                    return $url;
                } else {
                    if (isset($checkMedia['post_type']) && $checkMedia['post_type'] == 'attachment' && isset($checkMedia['post_mime_type']) ) {
                        $checkApp = strpos($checkMedia['post_mime_type'], 'application');

                        if ($checkApp === false) {
                            return $url;
                        } else {
                            $_snx_id = $this->scUpdateAttachUrl($match[3], $match[4]);
                            $rand_string = $this->scRandString(10);
                            // s=site no, id=sc attachment_id, aid=post type attachment type id
                            $param = $rand_string.base64_encode('s='.$match[3].'&id='.$_snx_id.'&a='.$post_id);
                            // return $match[1].'/sc_download.php?s='.$match[3].'&id='.$_snx_id;
                            return $match[1].'/sc_download.php?fid='.$param;
                        }
                    } else {
                        return $url;
                    }
                }


这样重写完, 页面输出的url就已经变掉了.


解下来是sc_download.php文件. 因为我把传入的参数base64加密了一次,并且混入了10个随机的字符,保证url的唯一和随机性. 所以这里要处理下这个逻辑,把原来的参数要处理解析出来.

其他没什么好说的了, 唯一就是header的转发, 在header的Content-Disposition : attachment 无效, WP屏蔽了直接附件的下载. 所以修改下, 修改后完整的header下载是这样

其中$file_path就是实际的文件路径. 需要加上status_header(200)才能保证attachment作为附件的方式下载, 否则就只有浏览器直接打开的inline方式了.

在header发送前就可以作权限处理.模板之类的数据处理. 这里就省略掉了. 

@ob_end_clean();
status_header(200);
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private", false);
if (!(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on" && preg_match("/MSIE/", $_SERVER["HTTP_USER_AGENT"]))) {
    header('Pragma: no-cache');
}
header("Content-Type: application/octet-stream");
header("Content-Type: application/force-download");
header("Content-Type: ".mime_content_type($file_path));
header("Content-Disposition: attachment; filename=\"".urlencode(basename($file_path))."\"");
//header("Content-Disposition: inline; filename=\"".urlencode(basename($file_path))."\";");
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".filesize($file_path));
ob_clean();
flush();
readfile("$file_path");
exit;


[自己动手改wordpress.3]给wordpress加上禁止附件外链