引言

下载了一个 Typecho的插件,用来备份数据库数据的,好奇研究了一下,发现里面的代码还挺简单的,但有些是 Typecho的插件写法,遂记录下来。

注:讲解的插件官网源码会有一些代码缩进问题,这里统一以正常的缩进结构来讲解

这里附上插件源码链接:

Typecho插件AutoBackup源码

代码讲解

目录结构

下载下来的目录大概是这个样子,下面开始细致讲解这个目录下的文件内容。

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行就是激活代码。

  1. 第10行第11行:注册文章发布、评论接口,如果有文章发布或评论时,调用备份的 render方法
  2. 第12行:注册一个路由配置,使得访问博客 域名/autobackup的请求,被定向到 Action.phpaction方法中。

小提示:

  1. 文章发布指的是文章新增和文章修改,都属于重新发布的概念
  2. 插件备份的 render方法内会引用 send.php文件进行备份并发送邮件
  3. 注册 Typecho博客的接口监听教程,可参考:插件基础开发
  4. 具体有哪些接口可供注册,可参考:插件接口列表

这里我不大理解这个接口注册为什么是写成 write_15finishComment_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.phpaction方法,检查了秘钥信息,然后调用 send.phpsender方法。

另一种是激活插件时说的,文章发布的时候,会自动执行 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.phpsender方法。

接下来就细致讲解这部分的代,代码上,原作者没有进行拆分,但逻辑上实际上是可以拆开的

所以这部分我准备分成下面四部分讲解:

  1. 使用上次备份时间和备份周期 读取、判断、更新
  2. 备份 sql数据获取

    1. Typecho当中读取表结构和相关数据
    2. 使用 zip压缩数据
  3. smtp发送邮件
  4. 删除备份文件

这里的删除备份文件就不做赘述了,无非就是一个 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就是获取插件配置的方式,同样也可以获取其他的配置值。

然后用了一个 TypechoDb类实例获取

$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的数组,包括以下的内容

$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,讲完,瑞思拜~


欢迎关注拓行公众号,分享各种技术博客文章

拓行——奋勇进取,开拓未来,砥砺前行

最后修改:2024 年 06 月 17 日
如果您对各种技术博客文章感兴趣,欢迎关注拓行公众号,分享各种专业技术知识~