123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- <?php
- namespace Widget;
- use Typecho\Common;
- use Typecho\Cookie;
- use Typecho\Exception;
- use Typecho\Plugin;
- use Widget\Base\Options as BaseOptions;
- if (!defined('__TYPECHO_ROOT_DIR__')) {
- exit;
- }
- /**
- * 备份工具
- *
- * @package Widget
- */
- class Backup extends BaseOptions implements ActionInterface
- {
- public const HEADER = '%TYPECHO_BACKUP_XXXX%';
- public const HEADER_VERSION = '0001';
- /**
- * @var array
- */
- private $types = [
- 'contents' => 1,
- 'comments' => 2,
- 'metas' => 3,
- 'relationships' => 4,
- 'users' => 5,
- 'fields' => 6
- ];
- /**
- * @var array
- */
- private $fields = [
- 'contents' => [
- 'cid', 'title', 'slug', 'created', 'modified', 'text', 'order', 'authorId',
- 'template', 'type', 'status', 'password', 'commentsNum', 'allowComment', 'allowPing', 'allowFeed', 'parent'
- ],
- 'comments' => [
- 'coid', 'cid', 'created', 'author', 'authorId', 'ownerId',
- 'mail', 'url', 'ip', 'agent', 'text', 'type', 'status', 'parent'
- ],
- 'metas' => [
- 'mid', 'name', 'slug', 'type', 'description', 'count', 'order', 'parent'
- ],
- 'relationships' => ['cid', 'mid'],
- 'users' => [
- 'uid', 'name', 'password', 'mail', 'url', 'screenName',
- 'created', 'activated', 'logged', 'group', 'authCode'
- ],
- 'fields' => [
- 'cid', 'name', 'type', 'str_value', 'int_value', 'float_value'
- ]
- ];
- /**
- * @var array
- */
- private $lastIds = [];
- /**
- * @var array
- */
- private $cleared = [];
- /**
- * @var bool
- */
- private $login = false;
- /**
- * 列出已有备份文件
- *
- * @return array
- */
- public function listFiles(): array
- {
- return array_map('basename', glob(__TYPECHO_BACKUP_DIR__ . '/*.dat'));
- }
- /**
- * 绑定动作
- */
- public function action()
- {
- $this->user->pass('administrator');
- $this->security->protect();
- $this->on($this->request->is('do=export'))->export();
- $this->on($this->request->is('do=import'))->import();
- }
- /**
- * 导出数据
- *
- * @throws \Typecho\Db\Exception
- */
- private function export()
- {
- $backupFile = tempnam(sys_get_temp_dir(), 'backup_');
- $fp = fopen($backupFile, 'wb');
- $host = parse_url($this->options->siteUrl, PHP_URL_HOST);
- $this->response->setContentType('application/octet-stream');
- $this->response->setHeader('Content-Disposition', 'attachment; filename="'
- . date('Ymd') . '_' . $host . '_' . uniqid() . '.dat"');
- $header = str_replace('XXXX', self::HEADER_VERSION, self::HEADER);
- fwrite($fp, $header);
- $db = $this->db;
- foreach ($this->types as $type => $val) {
- $page = 1;
- do {
- $rows = $db->fetchAll($db->select()->from('table.' . $type)->page($page, 20));
- $page++;
- foreach ($rows as $row) {
- fwrite($fp, $this->buildBuffer($val, $this->applyFields($type, $row)));
- }
- } while (count($rows) == 20);
- }
- self::pluginHandle()->export($fp);
- fwrite($fp, $header);
- fclose($fp);
- $this->response->throwFile($backupFile, 'application/octet-stream');
- }
- /**
- * @param $type
- * @param $data
- * @return string
- */
- private function buildBuffer($type, $data): string
- {
- $body = '';
- $schema = [];
- foreach ($data as $key => $val) {
- $schema[$key] = null === $val ? null : strlen($val);
- $body .= $val;
- }
- $header = json_encode($schema);
- return Common::buildBackupBuffer($type, $header, $body);
- }
- /**
- * 过滤字段
- *
- * @param $table
- * @param $data
- * @return array
- */
- private function applyFields($table, $data): array
- {
- $result = [];
- foreach ($data as $key => $val) {
- $index = array_search($key, $this->fields[$table]);
- if ($index !== false) {
- $result[$key] = $val;
- if ($index === 0 && !in_array($table, ['relationships', 'fields'])) {
- $this->lastIds[$table] = isset($this->lastIds[$table])
- ? max($this->lastIds[$table], $val) : $val;
- }
- }
- }
- return $result;
- }
- /**
- * 导入数据
- */
- private function import()
- {
- $path = null;
- if (!empty($_FILES)) {
- $file = array_pop($_FILES);
- if(UPLOAD_ERR_NO_FILE == $file['error']) {
- Notice::alloc()->set(_t('没有选择任何备份文件'), 'error');
- $this->response->goBack();
- }
- if (UPLOAD_ERR_OK == $file['error'] && is_uploaded_file($file['tmp_name'])) {
- $path = $file['tmp_name'];
- } else {
- Notice::alloc()->set(_t('备份文件上传失败'), 'error');
- $this->response->goBack();
- }
- } else {
- if (!$this->request->is('file')) {
- Notice::alloc()->set(_t('没有选择任何备份文件'), 'error');
- $this->response->goBack();
- }
- $path = __TYPECHO_BACKUP_DIR__ . '/' . $this->request->get('file');
- if (!file_exists($path)) {
- Notice::alloc()->set(_t('备份文件不存在'), 'error');
- $this->response->goBack();
- }
- }
- $this->extractData($path);
- }
- /**
- * 解析数据
- *
- * @param $file
- * @throws \Typecho\Db\Exception
- */
- private function extractData($file)
- {
- $fp = @fopen($file, 'rb');
- if (!$fp) {
- Notice::alloc()->set(_t('无法读取备份文件'), 'error');
- $this->response->goBack();
- }
- $fileSize = filesize($file);
- $headerSize = strlen(self::HEADER);
- if ($fileSize < $headerSize) {
- @fclose($fp);
- Notice::alloc()->set(_t('备份文件格式错误'), 'error');
- $this->response->goBack();
- }
- $fileHeader = @fread($fp, $headerSize);
- if (!$this->parseHeader($fileHeader, $version)) {
- @fclose($fp);
- Notice::alloc()->set(_t('备份文件格式错误'), 'error');
- $this->response->goBack();
- }
- fseek($fp, $fileSize - $headerSize);
- $fileFooter = @fread($fp, $headerSize);
- if (!$this->parseHeader($fileFooter, $version)) {
- @fclose($fp);
- Notice::alloc()->set(_t('备份文件格式错误'), 'error');
- $this->response->goBack();
- }
- fseek($fp, $headerSize);
- $offset = $headerSize;
- while (!feof($fp) && $offset + $headerSize < $fileSize) {
- $data = Common::extractBackupBuffer($fp, $offset, $version);
- if (!$data) {
- @fclose($fp);
- Notice::alloc()->set(_t('恢复数据出现错误'), 'error');
- $this->response->goBack();
- }
- [$type, $header, $body] = $data;
- $this->processData($type, $header, $body);
- }
- // 针对PGSQL重置计数
- if (false !== strpos(strtolower($this->db->getAdapterName()), 'pgsql')) {
- foreach ($this->lastIds as $table => $id) {
- $seq = $this->db->getPrefix() . $table . '_seq';
- $this->db->query('ALTER SEQUENCE ' . $seq . ' RESTART WITH ' . ($id + 1));
- }
- }
- @fclose($fp);
- Notice::alloc()->set(_t('数据恢复完成'), 'success');
- $this->response->goBack();
- }
- /**
- * @param $str
- * @param $version
- * @return bool
- */
- private function parseHeader($str, &$version): bool
- {
- if (!$str || strlen($str) != strlen(self::HEADER)) {
- return false;
- }
- if (!preg_match("/%TYPECHO_BACKUP_[A-Z0-9]{4}%/", $str)) {
- return false;
- }
- $version = substr($str, 16, - 1);
- return true;
- }
- /**
- * @param $type
- * @param $header
- * @param $body
- */
- private function processData($type, $header, $body)
- {
- $table = array_search($type, $this->types);
- if (!empty($table)) {
- $schema = json_decode($header, true);
- $data = [];
- $offset = 0;
- foreach ($schema as $key => $val) {
- $data[$key] = null === $val ? null : substr($body, $offset, $val);
- $offset += $val;
- }
- $this->importData($table, $data);
- } else {
- self::pluginHandle()->import($type, $header, $body);
- }
- }
- /**
- * 导入单条数据
- *
- * @param $table
- * @param $data
- */
- private function importData($table, $data)
- {
- $db = $this->db;
- try {
- if (empty($this->cleared[$table])) {
- // 清除数据
- $db->truncate('table.' . $table);
- $this->cleared[$table] = true;
- }
- if (!$this->login && 'users' == $table && $data['group'] == 'administrator') {
- // 重新登录
- $this->reLogin($data);
- }
- $db->query($db->insert('table.' . $table)->rows($this->applyFields($table, $data)));
- } catch (Exception $e) {
- Notice::alloc()->set(_t('恢复过程中遇到如下错误: %s', $e->getMessage()), 'error');
- $this->response->goBack();
- }
- }
- /**
- * 备份过程会重写用户数据
- * 所以需要重新登录当前用户
- *
- * @param $user
- */
- private function reLogin(&$user)
- {
- if (empty($user['authCode'])) {
- $user['authCode'] = function_exists('openssl_random_pseudo_bytes') ?
- bin2hex(openssl_random_pseudo_bytes(16)) : sha1(Common::randString(20));
- }
- $user['activated'] = $this->options->time;
- $user['logged'] = $user['activated'];
- Cookie::set('__typecho_uid', $user['uid']);
- Cookie::set('__typecho_authCode', Common::hash($user['authCode']));
- $this->login = true;
- }
- }
|