Backup.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. namespace Widget;
  3. use Typecho\Common;
  4. use Typecho\Cookie;
  5. use Typecho\Exception;
  6. use Typecho\Plugin;
  7. use Widget\Base\Options as BaseOptions;
  8. if (!defined('__TYPECHO_ROOT_DIR__')) {
  9. exit;
  10. }
  11. /**
  12. * 备份工具
  13. *
  14. * @package Widget
  15. */
  16. class Backup extends BaseOptions implements ActionInterface
  17. {
  18. public const HEADER = '%TYPECHO_BACKUP_XXXX%';
  19. public const HEADER_VERSION = '0001';
  20. /**
  21. * @var array
  22. */
  23. private $types = [
  24. 'contents' => 1,
  25. 'comments' => 2,
  26. 'metas' => 3,
  27. 'relationships' => 4,
  28. 'users' => 5,
  29. 'fields' => 6
  30. ];
  31. /**
  32. * @var array
  33. */
  34. private $fields = [
  35. 'contents' => [
  36. 'cid', 'title', 'slug', 'created', 'modified', 'text', 'order', 'authorId',
  37. 'template', 'type', 'status', 'password', 'commentsNum', 'allowComment', 'allowPing', 'allowFeed', 'parent'
  38. ],
  39. 'comments' => [
  40. 'coid', 'cid', 'created', 'author', 'authorId', 'ownerId',
  41. 'mail', 'url', 'ip', 'agent', 'text', 'type', 'status', 'parent'
  42. ],
  43. 'metas' => [
  44. 'mid', 'name', 'slug', 'type', 'description', 'count', 'order', 'parent'
  45. ],
  46. 'relationships' => ['cid', 'mid'],
  47. 'users' => [
  48. 'uid', 'name', 'password', 'mail', 'url', 'screenName',
  49. 'created', 'activated', 'logged', 'group', 'authCode'
  50. ],
  51. 'fields' => [
  52. 'cid', 'name', 'type', 'str_value', 'int_value', 'float_value'
  53. ]
  54. ];
  55. /**
  56. * @var array
  57. */
  58. private $lastIds = [];
  59. /**
  60. * @var array
  61. */
  62. private $cleared = [];
  63. /**
  64. * @var bool
  65. */
  66. private $login = false;
  67. /**
  68. * 列出已有备份文件
  69. *
  70. * @return array
  71. */
  72. public function listFiles(): array
  73. {
  74. return array_map('basename', glob(__TYPECHO_BACKUP_DIR__ . '/*.dat'));
  75. }
  76. /**
  77. * 绑定动作
  78. */
  79. public function action()
  80. {
  81. $this->user->pass('administrator');
  82. $this->security->protect();
  83. $this->on($this->request->is('do=export'))->export();
  84. $this->on($this->request->is('do=import'))->import();
  85. }
  86. /**
  87. * 导出数据
  88. *
  89. * @throws \Typecho\Db\Exception
  90. */
  91. private function export()
  92. {
  93. $backupFile = tempnam(sys_get_temp_dir(), 'backup_');
  94. $fp = fopen($backupFile, 'wb');
  95. $host = parse_url($this->options->siteUrl, PHP_URL_HOST);
  96. $this->response->setContentType('application/octet-stream');
  97. $this->response->setHeader('Content-Disposition', 'attachment; filename="'
  98. . date('Ymd') . '_' . $host . '_' . uniqid() . '.dat"');
  99. $header = str_replace('XXXX', self::HEADER_VERSION, self::HEADER);
  100. fwrite($fp, $header);
  101. $db = $this->db;
  102. foreach ($this->types as $type => $val) {
  103. $page = 1;
  104. do {
  105. $rows = $db->fetchAll($db->select()->from('table.' . $type)->page($page, 20));
  106. $page++;
  107. foreach ($rows as $row) {
  108. fwrite($fp, $this->buildBuffer($val, $this->applyFields($type, $row)));
  109. }
  110. } while (count($rows) == 20);
  111. }
  112. self::pluginHandle()->export($fp);
  113. fwrite($fp, $header);
  114. fclose($fp);
  115. $this->response->throwFile($backupFile, 'application/octet-stream');
  116. }
  117. /**
  118. * @param $type
  119. * @param $data
  120. * @return string
  121. */
  122. private function buildBuffer($type, $data): string
  123. {
  124. $body = '';
  125. $schema = [];
  126. foreach ($data as $key => $val) {
  127. $schema[$key] = null === $val ? null : strlen($val);
  128. $body .= $val;
  129. }
  130. $header = json_encode($schema);
  131. return Common::buildBackupBuffer($type, $header, $body);
  132. }
  133. /**
  134. * 过滤字段
  135. *
  136. * @param $table
  137. * @param $data
  138. * @return array
  139. */
  140. private function applyFields($table, $data): array
  141. {
  142. $result = [];
  143. foreach ($data as $key => $val) {
  144. $index = array_search($key, $this->fields[$table]);
  145. if ($index !== false) {
  146. $result[$key] = $val;
  147. if ($index === 0 && !in_array($table, ['relationships', 'fields'])) {
  148. $this->lastIds[$table] = isset($this->lastIds[$table])
  149. ? max($this->lastIds[$table], $val) : $val;
  150. }
  151. }
  152. }
  153. return $result;
  154. }
  155. /**
  156. * 导入数据
  157. */
  158. private function import()
  159. {
  160. $path = null;
  161. if (!empty($_FILES)) {
  162. $file = array_pop($_FILES);
  163. if(UPLOAD_ERR_NO_FILE == $file['error']) {
  164. Notice::alloc()->set(_t('没有选择任何备份文件'), 'error');
  165. $this->response->goBack();
  166. }
  167. if (UPLOAD_ERR_OK == $file['error'] && is_uploaded_file($file['tmp_name'])) {
  168. $path = $file['tmp_name'];
  169. } else {
  170. Notice::alloc()->set(_t('备份文件上传失败'), 'error');
  171. $this->response->goBack();
  172. }
  173. } else {
  174. if (!$this->request->is('file')) {
  175. Notice::alloc()->set(_t('没有选择任何备份文件'), 'error');
  176. $this->response->goBack();
  177. }
  178. $path = __TYPECHO_BACKUP_DIR__ . '/' . $this->request->get('file');
  179. if (!file_exists($path)) {
  180. Notice::alloc()->set(_t('备份文件不存在'), 'error');
  181. $this->response->goBack();
  182. }
  183. }
  184. $this->extractData($path);
  185. }
  186. /**
  187. * 解析数据
  188. *
  189. * @param $file
  190. * @throws \Typecho\Db\Exception
  191. */
  192. private function extractData($file)
  193. {
  194. $fp = @fopen($file, 'rb');
  195. if (!$fp) {
  196. Notice::alloc()->set(_t('无法读取备份文件'), 'error');
  197. $this->response->goBack();
  198. }
  199. $fileSize = filesize($file);
  200. $headerSize = strlen(self::HEADER);
  201. if ($fileSize < $headerSize) {
  202. @fclose($fp);
  203. Notice::alloc()->set(_t('备份文件格式错误'), 'error');
  204. $this->response->goBack();
  205. }
  206. $fileHeader = @fread($fp, $headerSize);
  207. if (!$this->parseHeader($fileHeader, $version)) {
  208. @fclose($fp);
  209. Notice::alloc()->set(_t('备份文件格式错误'), 'error');
  210. $this->response->goBack();
  211. }
  212. fseek($fp, $fileSize - $headerSize);
  213. $fileFooter = @fread($fp, $headerSize);
  214. if (!$this->parseHeader($fileFooter, $version)) {
  215. @fclose($fp);
  216. Notice::alloc()->set(_t('备份文件格式错误'), 'error');
  217. $this->response->goBack();
  218. }
  219. fseek($fp, $headerSize);
  220. $offset = $headerSize;
  221. while (!feof($fp) && $offset + $headerSize < $fileSize) {
  222. $data = Common::extractBackupBuffer($fp, $offset, $version);
  223. if (!$data) {
  224. @fclose($fp);
  225. Notice::alloc()->set(_t('恢复数据出现错误'), 'error');
  226. $this->response->goBack();
  227. }
  228. [$type, $header, $body] = $data;
  229. $this->processData($type, $header, $body);
  230. }
  231. // 针对PGSQL重置计数
  232. if (false !== strpos(strtolower($this->db->getAdapterName()), 'pgsql')) {
  233. foreach ($this->lastIds as $table => $id) {
  234. $seq = $this->db->getPrefix() . $table . '_seq';
  235. $this->db->query('ALTER SEQUENCE ' . $seq . ' RESTART WITH ' . ($id + 1));
  236. }
  237. }
  238. @fclose($fp);
  239. Notice::alloc()->set(_t('数据恢复完成'), 'success');
  240. $this->response->goBack();
  241. }
  242. /**
  243. * @param $str
  244. * @param $version
  245. * @return bool
  246. */
  247. private function parseHeader($str, &$version): bool
  248. {
  249. if (!$str || strlen($str) != strlen(self::HEADER)) {
  250. return false;
  251. }
  252. if (!preg_match("/%TYPECHO_BACKUP_[A-Z0-9]{4}%/", $str)) {
  253. return false;
  254. }
  255. $version = substr($str, 16, - 1);
  256. return true;
  257. }
  258. /**
  259. * @param $type
  260. * @param $header
  261. * @param $body
  262. */
  263. private function processData($type, $header, $body)
  264. {
  265. $table = array_search($type, $this->types);
  266. if (!empty($table)) {
  267. $schema = json_decode($header, true);
  268. $data = [];
  269. $offset = 0;
  270. foreach ($schema as $key => $val) {
  271. $data[$key] = null === $val ? null : substr($body, $offset, $val);
  272. $offset += $val;
  273. }
  274. $this->importData($table, $data);
  275. } else {
  276. self::pluginHandle()->import($type, $header, $body);
  277. }
  278. }
  279. /**
  280. * 导入单条数据
  281. *
  282. * @param $table
  283. * @param $data
  284. */
  285. private function importData($table, $data)
  286. {
  287. $db = $this->db;
  288. try {
  289. if (empty($this->cleared[$table])) {
  290. // 清除数据
  291. $db->truncate('table.' . $table);
  292. $this->cleared[$table] = true;
  293. }
  294. if (!$this->login && 'users' == $table && $data['group'] == 'administrator') {
  295. // 重新登录
  296. $this->reLogin($data);
  297. }
  298. $db->query($db->insert('table.' . $table)->rows($this->applyFields($table, $data)));
  299. } catch (Exception $e) {
  300. Notice::alloc()->set(_t('恢复过程中遇到如下错误: %s', $e->getMessage()), 'error');
  301. $this->response->goBack();
  302. }
  303. }
  304. /**
  305. * 备份过程会重写用户数据
  306. * 所以需要重新登录当前用户
  307. *
  308. * @param $user
  309. */
  310. private function reLogin(&$user)
  311. {
  312. if (empty($user['authCode'])) {
  313. $user['authCode'] = function_exists('openssl_random_pseudo_bytes') ?
  314. bin2hex(openssl_random_pseudo_bytes(16)) : sha1(Common::randString(20));
  315. }
  316. $user['activated'] = $this->options->time;
  317. $user['logged'] = $user['activated'];
  318. Cookie::set('__typecho_uid', $user['uid']);
  319. Cookie::set('__typecho_authCode', Common::hash($user['authCode']));
  320. $this->login = true;
  321. }
  322. }