引言
下载了一个 Typecho
的插件,用来备份数据库数据的,好奇研究了一下,发现里面的代码还挺简单的,但有些是 Typecho
的插件写法,遂记录下来。
注:讲解的插件官网源码会有一些代码缩进问题,这里统一以正常的缩进结构来讲解
这里附上插件源码链接:
Typecho插件AutoBackup源码
代码讲解
目录结构
下载下来的目录大概是这个样子,下面开始细致讲解这个目录下的文件内容。
// php发送邮件的支持类库文件夹
¸PHPMailer/
// 插件的主文件
Plugin.php
// Router请求入口
Action.php
// 每次发送的时间记录文件
config.xml
// php生成zip压缩包的类库文件
pclzip.lib.php
// 实际备份、发送邮件的核心代码
send.php
// 自述文件
README.md
那些三方支持类库文件(如 pclzip.lib.php
和 .PHPMailer/
),这里就不作细致的讲解了,仅说明用法。
主体文件(Plugin.php)
这个文件整个插件的灵魂,核心骨架。
备份请求目标入口(Action.php)
这里是备份请求发起(进行备份操作)的入口位置。
这个插件是以 Http
请求,作为触发条件的。
所以会导致程序如果没有在 30秒
内返回,那么客户端将无法收到响应。
注:博客内容较少时问题不大,一旦数据体量过大,就会导致发送失败
备份及发送核心代码(send.php)
这部分是整个插件的核心,核心方法为:sender
逻辑顺序讲解程序结构
1. 激活插件
首先,插件会按照 插件名_Plugin
类下的 activate
方法,去找到激活插件执行的方法
也就是插件下的 Plugin.php
文件的 activate
方法
/**
* 激活插件方法,如果激活失败,直接抛出异常
*
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function activate()
{
Typecho_Plugin::factory('Widget_Contents_Post_Edit')->write_15 = array('AutoBackup_Plugin', 'render');
Typecho_Plugin::factory('Widget_Feedback')->finishComment_15 = array('AutoBackup_Plugin', 'render');
Helper::addRoute("route_autobackup","/autobackup","AutoBackup_Action",'action');
}
这里的 第10行
、第11行
、第12行
就是激活代码。
第10行
、第11行
:注册文章发布、评论接口,如果有文章发布或评论时,调用备份的render
方法第12行
:注册一个路由配置,使得访问博客域名/autobackup
的请求,被定向到Action.php
的action
方法中。
小提示:
这里我不大理解这个接口注册为什么是写成 write_15
和 finishComment_15
如果有大佬看到这个,还请不吝赐教。
默认路由的解析,是解析文章和分类、独立页面等的
这里 第12行
的代码是增加了一个路由解析配置,在访问博客的请求中,匹配目标字串——“/autobackup
”,如果匹配得到,那就交由 Action.php
当中的 action
方法来处理。
这里的写法是 Typecho
插件规范要求,就不做详述了。
另外插一嘴,如果这个插件被禁用的话,会调用到 deactivate
方法,执行里头的 removeRoute
方法,用来销毁上述路由配置。
/**
* 禁用插件方法,如果禁用失败,直接抛出异常
*
* @static
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function deactivate()
{
Helper::removeRoute("route_autobackup");
}
2. 配置插件内容
一个插件,无可避免地需要用到很多的自定义配置。
如这个插件当中,就需要配置发送的邮件目标地址、发件人的邮箱信息等
这些配置项就需要在 config
方法当中实现了。
关注一下插件内的 public static function config(Typecho_Widget_Helper_Form $form)
方法
以其中一个配置项来讲解:
$tables = new Typecho_Widget_Helper_Form_Element_Checkbox('tables', self::listTables(), self::listTables(), _t('需要备份的数据表'), _t('选择你需要备份的数据表,插件首次启动时会默认全选'));
$form -> addInput($tables);
这里实例化了一个类,叫做 Typecho_Widget_Helper_Form_Element_Checkbox
这个类是 Typecho
提供的,用来创建一个表单组件
类似的还有:
序号 | 含义 | 类名 |
---|---|---|
1 | 复选框 | Typecho_Widget_Helper_Form_Element_Checkbox |
2 | 普通文本框 | Typecho_Widget_Helper_Form_Element_Text |
3 | 密码输入框 | Typecho_Widget_Helper_Form_Element_Password |
4 | 单选框 | Typecho_Widget_Helper_Form_Element_Radio |
- 第一个参数是:存入插件的这个配置项的名称
- 第二个参数是:提供选择或填写的数据
- 第三个参数是:默认选中的数据或默认填入的数据
- 第四个参数是:表单组件的提示文本,注意,加上
_t()
可以实现多语言 - 第四个参数是:表单组件的备注信息
后面,使用了 Typecho_Widget_Helper_Form $form
实例调用了 addInput
方法
将这个表单组件混入到这个插件当中。
这里提到的 rooturl
,指的是所有访问博客请求的基础链接地址。
$rooturl = Helper::options() -> rootUrl;
if (Helper::options() -> rewrite == 0){
$rooturl = $rooturl . '/index.php';
}
这段代码是用了 Typecho
提供的 Helper
助手工具类当中的配置项获取,拿到了基础 url
,包括了请求的 http/https
协议头,域名,前缀等
然后判断了是否开启了 地址重写 ,如果未开启,就将 index.php
拼接上去。
在后台的 设置
- 永久链接
当中可以看到是否开启了地址重写
如下图
3. 操作的请求方式
这里开始备份有两个启动方式,一个是直接从地址栏访问,如下图
这是由 addRoute
当中指向的 AutoBackup_Action
类中的 action
方法。
这里指向了一个 Action.php
的 action
方法,检查了秘钥信息,然后调用 send.php
的 sender
方法。
另一种是激活插件时说的,文章发布的时候,会自动执行 render
方法,这里也是调用了备份操作。
public static function render($contents, $inst)
{
if (Helper::options() -> plugin('AutoBackup') -> blogcron == '0') {
return $contents;
} else {
require_once 'send.php';
$send = new Send();
return $send->sender($contents, $inst);
}
}
这里的判断,主要是检测用户的配置项,是否需要监听文章接口,如果需要,则进行备份,否则文章发布时就不处理,直接将文章内容原样返回即可(这里可以修改文章内容)
至于这里的 第7行
当中的 require_once
,就是实际要调用的备份代码。
4. 备份操作与邮件发送
不管是 addRoute
当中指向的 AutoBackup_Action
类中的 action
方法,还是 render
方法当中直接 require_once
进来后调用的方法,实际上都是在调用 send.php
的 sender
方法。
接下来就细致讲解这部分的代,代码上,原作者没有进行拆分,但逻辑上实际上是可以拆开的
所以这部分我准备分成下面四部分讲解:
- 使用上次备份时间和备份周期 读取、判断、更新
备份
sql
数据获取- 从
Typecho
当中读取表结构和相关数据 - 使用
zip
压缩数据
- 从
smtp
发送邮件- 删除备份文件
这里的删除备份文件就不做赘述了,无非就是一个 unlink
方法来删掉这个备份数据文件
4.1 备份周期判断
插件目录下,有一个 config.xml
文件,这里面就是写着每次备份更新的时间戳,如下:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<lasttime>1716278902</lasttime>
</config>
这里的 lasttime
就是最近一次更新的时间戳(秒时间戳)
脚本当中,用了 simplexml_load_file
这个函数来读取上面这个配置文件的内容,函数的返回值是 xml
对象,可以通过对象取值的方式获取到里面的这个 lasttime
值。
如果这个时间在备份周期以内的话,就不进行备份了。
if ($type==0){
if ($lasttime < 0 || ($current - $lasttime) < $configs -> circle * 24 * 60 * 60) {
return $contents;
}
}
这里有个 type
判断,我根据上下文理解,应该是监听文章提交接口的一个参数,含义是文章发布。
4.2 备份数据获取
这里主要是用了这个 create_sql
方法进行获取,这个返回值是备份文件的路径(这里是完整路径)
$file_path = self::create_sql(); //获取备份语句
这里通过 Typecho
提供的助手类,获取了插件配置的表列表
$configs = Helper::options() -> plugin('AutoBackup');
$tables = $configs -> tables;
如果为空,则阻止后续执行
这里的 $configs -> tables
就是获取插件配置的方式,同样也可以获取其他的配置值。
然后用了一个 Typecho
的 Db
类实例获取
$db = Typecho_Db::get();
拿到实例操作对象,循环获取表结构(这里的循环数组是插件配置当中定义的数组)
// 执行SQL语句,query方法类似于mysqli_query()
$result = $db -> query("SHOW CREATE TABLE `" . $table . "`");
// 拿到执行结果,
$row = $db -> fetchRow($result);
这里的 $row
就是表结构,具体结构是数组:
array(2) {
["Table"]=>
string(16) "typecho_hw_users"
["Create Table"]=>
string(627) "CREATE TABLE `typecho_hw_users` (
`uid` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL,
`mail` varchar(150) DEFAULT NULL,
`url` varchar(150) DEFAULT NULL,
`screenName` varchar(32) DEFAULT NULL,
`created` int unsigned DEFAULT '0',
`activated` int unsigned DEFAULT '0',
`logged` int unsigned DEFAULT '0',
`group` varchar(16) DEFAULT 'visitor',
`authCode` varchar(64) DEFAULT NULL,
PRIMARY KEY (`uid`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `mail` (`mail`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"
}
其中,"Create Table"
就是建表语句
在建表语句前,拼接了一句 "\r\nDROP TABLE IF EXISTS ".$table.";\r\n"
代表如果表存在,那就先删除,否则就直接创建。
接下来使用了 select()
方法来获取这个表里的所有数据(我认为不大好,如果博客数据体量足够大,那这个地方就会导致内存占用特别严重)
// 获取表中所有数据
$tableAllData = $db -> select() -> from($table)
// 资源集转换为数组
$result = $db -> query($tableAllData);
这里的变量是我方便大家理解加上的,插件内容里没有这个变量定义
接下来使用一个 while
循环将这个数组进行完全遍历
while ($row = $db -> fetchRow($result)) {
// do something
}
循环内的每一次都代表了当前循环表当中的一条数据,在循环内进行数据字段的获取和处理
foreach ($row as $key => $value) { //每次取一行数据
$keys[] = "`" . $key . "`"; //字段存入数组
$values[] = "'" . addslashes($value) . "'"; //值存入数组
}
这里用到的 addslashs()
函数是在指定的预定义字符前添加反斜杠
之后将数据插入语句进行拼接,就是上面准备的数据,拼成一条插入语句(仅插入一条数据)
$sql .= "insert into `".$table."` (".implode(",", $keys).") values (".implode(",", $values).");\r\n"; //生成插入语句
然后将用到的这些变量清除,避免内存占用高
这里三层嵌套循环结束之后,$sql
就是所有的建表和插入数据的语句(字符串)
使用 SMTP密码 拼接 当前时间戳 的 MD5
值作为备份文件名,使用 file_put_contents()
函数写入文件当中。
// 获取即将写入的备份文件全路径
$file_path = dirname(__FILE__) . "/backupfiles/" . md5($configs -> pass . time()) . ".sql";
// 写入备份文件
file_put_contents($file_path, $sql);
如果当前不支持压缩,就不继续操作了
if (!function_exists('gzopen')) {
return $file_path;
}
如果支持压缩,就开始进行 zip
压缩
// 引入压缩的扩展包
require_once('pclzip.lib.php');
// 创建压缩对象及文件,规定存放路径
$zip = new PclZip(dirname(__FILE__) . "/backupfiles/" . md5($configs -> pass . time()) . ".zip");
// 将实际文件路径传入,并进行压缩
$zip -> create($file_path, PCLZIP_OPT_REMOVE_PATH, dirname(__FILE__) . "/backupfiles/");
// 压缩文件名(全路径)
$fileName = $zip->zipname;
压缩完之后,删除掉未压缩的原文件,然后将压缩后的路径返回出去。
4.3 SMTP
发送邮件
由于现在的主流邮箱,都有一些黑名单和安全过滤,如果使用 PHP
自带的 email
函数,或者 Linux
当中的邮件发送方法,大概率会被主流邮箱(如腾讯邮箱、网易邮箱、雅虎邮箱)拦截
所以现在业内发送邮件的常规做法是,使用一个正常的邮箱账号(我这里用的是腾讯邮箱),作为发送端,然后指定一个接收端邮箱(任何邮箱皆可)
发送端需要做到的事情是提供一个授权码,这个授权码的获取有两个教程:
一个是我的这篇内容:
一个是可以参考一下这篇博客当中的“SMTP配置”一节。
我们使用程序(调用 smtp
的类库)模拟,或者说是控制发送端进行邮件发送,这样就不会在黑名单之内了。
在这儿构建一个 SMTP
的数组,包括以下的内容
$smtp['site']
$smtp['attach']
$smtp['attach_name']
$smtp['attach_name']
$smtp['user']
$smtp['pass']
$smtp['host']
$smtp['port']
$smtp['subject']
$smtp['subject']
$smtp['AltBody']
$smtp['body']
其中,pass
就是邮箱授权码。
body
是邮件的主体内容,其中就有备份的时间、发件人的博客地址等
attach
就是将这个备份文件作为附件加入到邮件附件当中。
下面的这个 SendMail()
方法就是实际调用 SMTP
类库进行发送的,注意,这里的认证是一定要开启的,否则会被主流邮箱拦截。
ok,讲完,瑞思拜~