Loading... ## 引言 下载了一个 `Typecho`的插件,用来备份数据库数据的,好奇研究了一下,发现里面的代码还挺简单的,但有些是 `Typecho`的插件写法,遂记录下来。 **注:讲解的插件官网源码会有一些代码缩进问题,这里统一以正常的缩进结构来讲解** 这里附上插件源码链接: > [Typecho插件AutoBackup源码](https://github.com/typecho-fans/plugins/tree/master/AutoBackup) ## 代码讲解 ### 目录结构 下载下来的目录大概是这个样子,下面开始细致讲解这个目录下的文件内容。 ![AutoBackup插件结构](https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/699266984.png) ``` // php发送邮件的支持类库文件夹 ¸PHPMailer/ // 插件的主文件 Plugin.php // Router请求入口 Action.php // 每次发送的时间记录文件 config.xml // php生成zip压缩包的类库文件 pclzip.lib.php // 实际备份、发送邮件的核心代码 send.php // 自述文件 README.md ``` 那些三方支持类库文件(如 `pclzip.lib.php`和 `.PHPMailer/`),这里就不作细致的讲解了,仅说明用法。 #### 主体文件(Plugin.php) 这个文件整个插件的灵魂,核心骨架。 ![插件主体文件](https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/3648867973.png) #### 备份请求目标入口(Action.php) 这里是备份请求发起(进行备份操作)的入口位置。 这个插件是以 `Http`请求,作为触发条件的。 所以会导致程序如果没有在 `30秒`内返回,那么客户端将无法收到响应。 <span style='color:red'>**注:博客内容较少时问题不大,一旦数据体量过大,就会导致发送失败**</span> ![备份请求目标入口文件](https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/225942245.png) #### 备份及发送核心代码(send.php) 这部分是整个插件的核心,核心方法为:`sender` ![备份及发送核心代码](https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/1740036292.png) ### 逻辑顺序讲解程序结构 #### 1. 激活插件 首先,插件会按照 `插件名_Plugin`类下的 `activate`方法,去找到激活插件执行的方法 也就是插件下的 `Plugin.php`文件的 `activate`方法 ```php /** * 激活插件方法,如果激活失败,直接抛出异常 * * @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.php`的 `action`方法中。 > 小提示: > > 1. 文章发布指的是文章新增和文章修改,都属于重新发布的概念 > 2. 插件备份的 `render`方法内会引用 `send.php`文件进行备份并发送邮件 > 3. 注册 `Typecho`博客的接口监听教程,可参考:[插件基础开发](https://docs.typecho.org/plugins/hello-world#%E6%8F%92%E4%BB%B6%E5%88%86%E6%9E%90) > 4. 具体有哪些接口可供注册,可参考:[插件接口列表](https://docs.typecho.org/plugins/hooks) 这里我不大理解这个接口注册为什么是写成 `write_15`和 `finishComment_15` 如果有大佬看到这个,还请不吝赐教。 默认路由的解析,是解析文章和分类、独立页面等的 这里 `第12行`的代码是增加了一个路由解析配置,在访问博客的请求中,匹配目标字串——“`/autobackup`”,如果匹配得到,那就交由 `Action.php`当中的 `action`方法来处理。 这里的写法是 `Typecho`插件规范要求,就不做详述了。 另外插一嘴,如果这个插件被禁用的话,会调用到 `deactivate`方法,执行里头的 `removeRoute`方法,用来销毁上述路由配置。 ```php /** * 禁用插件方法,如果禁用失败,直接抛出异常 * * @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)`方法 以其中一个配置项来讲解: ```php $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`,指的是所有访问博客请求的基础链接地址。 ```php $rooturl = Helper::options() -> rootUrl; if (Helper::options() -> rewrite == 0){ $rooturl = $rooturl . '/index.php'; } ``` 这段代码是用了 `Typecho`提供的 `Helper`助手工具类当中的配置项获取,拿到了基础 `url`,包括了请求的 `http/https`协议头,域名,前缀等 然后判断了是否开启了 **地址重写** ,如果未开启,就将 `index.php`拼接上去。 ![永久链接菜单](https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/1396364563.png) 在后台的 `设置` - `永久链接`当中可以看到是否开启了地址重写 如下图 ![地址重写开关](https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/2113972629.png) #### 3. 操作的请求方式 这里开始备份有两个启动方式,一个是直接从地址栏访问,如下图 ![直接访问备份接口地址](https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/3024786276.png) 这是由 `addRoute`当中指向的 `AutoBackup_Action`类中的 `action`方法。 这里指向了一个 `Action.php`的 `action`方法,检查了秘钥信息,然后调用 `send.php`的 `sender`方法。 另一种是激活插件时说的,文章发布的时候,会自动执行 `render`方法,这里也是调用了备份操作。 ```php 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`方法。 接下来就细致讲解这部分的代,代码上,原作者没有进行拆分,但逻辑上实际上是可以拆开的 所以这部分我准备分成下面四部分讲解: 1. 使用上次备份时间和备份周期 读取、判断、更新 2. 备份 `sql`数据获取 1. 从 `Typecho`当中读取表结构和相关数据 2. 使用 `zip`压缩数据 3. `smtp`发送邮件 4. 删除备份文件 这里的删除备份文件就不做赘述了,无非就是一个 `unlink`方法来删掉这个备份数据文件 ##### 4.1 备份周期判断 插件目录下,有一个 `config.xml`文件,这里面就是写着每次备份更新的时间戳,如下: ```xml <?xml version="1.0" encoding="UTF-8"?> <config> <lasttime>1716278902</lasttime> </config> ``` 这里的 `lasttime`就是最近一次更新的时间戳(秒时间戳) 脚本当中,用了 `simplexml_load_file`这个函数来读取上面这个配置文件的内容,函数的返回值是 `xml`对象,可以通过对象取值的方式获取到里面的这个 `lasttime`值。 如果这个时间在备份周期以内的话,就不进行备份了。 ```php if ($type==0){ if ($lasttime < 0 || ($current - $lasttime) < $configs -> circle * 24 * 60 * 60) { return $contents; } } ``` 这里有个 `type`判断,我根据上下文理解,应该是监听文章提交接口的一个参数,含义是文章发布。 ##### 4.2 备份数据获取 这里主要是用了这个 `create_sql`方法进行获取,这个返回值是备份文件的路径(这里是完整路径) ```php $file_path = self::create_sql(); //获取备份语句 ``` 这里通过 `Typecho`提供的助手类,获取了插件配置的表列表 ```php $configs = Helper::options() -> plugin('AutoBackup'); $tables = $configs -> tables; ``` 如果为空,则阻止后续执行 这里的 `$configs -> tables`就是获取插件配置的方式,同样也可以获取其他的配置值。 然后用了一个 `Typecho`的 `Db`类实例获取 ```php $db = Typecho_Db::get(); ``` 拿到实例操作对象,循环获取表结构(这里的循环数组是插件配置当中定义的数组) ```php // 执行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()`方法来获取这个表里的所有数据(我认为不大好,如果博客数据体量足够大,那这个地方就会导致内存占用特别严重) ```php // 获取表中所有数据 $tableAllData = $db -> select() -> from($table) // 资源集转换为数组 $result = $db -> query($tableAllData); ``` **这里的变量是我方便大家理解加上的,插件内容里没有这个变量定义** 接下来使用一个 `while`循环将这个数组进行完全遍历 ```php while ($row = $db -> fetchRow($result)) { // do something } ``` 循环内的每一次都代表了当前循环表当中的一条数据,在循环内进行数据字段的获取和处理 ```php foreach ($row as $key => $value) { //每次取一行数据 $keys[] = "`" . $key . "`"; //字段存入数组 $values[] = "'" . addslashes($value) . "'"; //值存入数组 } ``` > 这里用到的 `addslashs()`函数是在指定的预定义字符前添加反斜杠 之后将数据插入语句进行拼接,就是上面准备的数据,拼成一条插入语句(仅插入一条数据) ```php $sql .= "insert into `".$table."` (".implode(",", $keys).") values (".implode(",", $values).");\r\n"; //生成插入语句 ``` 然后将用到的这些变量清除,避免内存占用高 --- 这里三层嵌套循环结束之后,`$sql`就是所有的建表和插入数据的语句(字符串) 使用 **SMTP密码 拼接 当前时间戳** 的 `MD5`值作为备份文件名,使用 `file_put_contents()`函数写入文件当中。 ```php // 获取即将写入的备份文件全路径 $file_path = dirname(__FILE__) . "/backupfiles/" . md5($configs -> pass . time()) . ".sql"; // 写入备份文件 file_put_contents($file_path, $sql); ``` 如果当前不支持压缩,就不继续操作了 ```php if (!function_exists('gzopen')) { return $file_path; } ``` 如果支持压缩,就开始进行 `zip`压缩 ```php // 引入压缩的扩展包 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`当中的邮件发送方法,大概率会被主流邮箱(如腾讯邮箱、网易邮箱、雅虎邮箱)拦截 所以现在业内发送邮件的常规做法是,使用一个正常的邮箱账号(我这里用的是腾讯邮箱),作为发送端,然后指定一个接收端邮箱(任何邮箱皆可) 发送端需要做到的事情是提供一个授权码,这个授权码的获取有两个教程: 一个是我的这篇内容: <div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="https://hw13.cn/465.html" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://aliyun-yuesha-public-oss.oss-cn-zhangjiakou.aliyuncs.com/usr/uploads/2024/06/1451868192.png);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">主流邮箱的SMTP设置及授权码获取(腾讯QQ邮箱为例)</p> <div class="inster-summary text-muted"> 引言程序发送邮件或者一些客户端使用的时候,都需要用到邮箱的授权码(有的说是邮箱的账号和密码,实际密码就是授权码而不... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> 一个是可以参考一下这篇博客当中的“SMTP配置”一节。 [安装自动备份插件-SMTP配置](https://lyj15.cn/283.html) 我们使用程序(调用 `smtp`的类库)模拟,或者说是控制发送端进行邮件发送,这样就不会在黑名单之内了。 在这儿构建一个 `SMTP`的数组,包括以下的内容 ```php $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 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果您对各种技术博客文章感兴趣,欢迎关注拓行公众号,分享各种专业技术知识~