UOJBlogEditor.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <?php
  2. class UOJBlogEditor {
  3. public $type = 'blog';
  4. public $name;
  5. public $blog_url;
  6. public $save;
  7. public $cur_data = array();
  8. public $post_data = array();
  9. public $label_text = array(
  10. 'title' => '标题',
  11. 'tags' => '标签(多个标签用逗号隔开)',
  12. 'content' => '内容',
  13. 'view blog' => '查看博客',
  14. 'blog visibility' => '博客可见性',
  15. 'private' => '未公开',
  16. 'public' => '公开'
  17. );
  18. public $validator = array();
  19. function __construct() {
  20. global $REQUIRE_LIB;
  21. $REQUIRE_LIB['blog-editor'] = '';
  22. $this->validator = array(
  23. 'title' => function(&$title) {
  24. if ($title == '') {
  25. return '标题不能为空';
  26. }
  27. if (strlen($title) > 100) {
  28. return '标题不能超过 100 个字节';
  29. }
  30. if (HTML::escape($title) === '') {
  31. return '无效编码';
  32. }
  33. return '';
  34. },
  35. 'content_md' => function(&$content_md) {
  36. if (strlen($content_md) > 1000000) {
  37. return '内容过长';
  38. }
  39. return '';
  40. },
  41. 'tags' => function(&$tags) {
  42. $tags = str_replace(',', ',', $tags);
  43. $tags_raw = explode(',', $tags);
  44. if (count($tags_raw) > 10) {
  45. return '标签个数不能超过10';
  46. }
  47. $tags = array();
  48. foreach ($tags_raw as $tag) {
  49. $tag = trim($tag);
  50. if (strlen($tag) == 0) {
  51. continue;
  52. }
  53. if (strlen($tag) > 30) {
  54. return '标签 “' . HTML::escape($tag) .'” 太长';
  55. }
  56. if (in_array($tag, $tags, true)) {
  57. return '标签 “' . HTML::escape($tag) .'” 重复出现';
  58. }
  59. $tags[] = $tag;
  60. }
  61. return '';
  62. }
  63. );
  64. }
  65. public function validate($name) {
  66. if (!isset($_POST["{$this->name}_{$name}"])) {
  67. return '不能为空';
  68. }
  69. $this->post_data[$name] = $_POST["{$this->name}_{$name}"];
  70. $val = $this->validator[$name];
  71. return $val($this->post_data[$name]);
  72. }
  73. private function receivePostData() {
  74. $errors = array();
  75. foreach (array('title', 'content_md', 'tags') as $name) {
  76. $cur_err = $this->validate($name);
  77. if ($cur_err) {
  78. $errors[$name] = $cur_err;
  79. }
  80. }
  81. if ($errors) {
  82. die(json_encode($errors));
  83. }
  84. crsf_defend();
  85. $this->post_data['is_hidden'] = isset($_POST["{$this->name}_is_hidden"]) ? 1 : 0;
  86. $purifier = HTML::pruifier();
  87. $this->post_data['title'] = HTML::escape($this->post_data['title']);
  88. if ($this->type == 'blog') {
  89. $content_md = $_POST[$this->name . '_content_md'];
  90. try {
  91. $v8 = new V8Js('POST');
  92. $v8->setTimeLimit(2000);
  93. $v8->content_md = $this->post_data['content_md'];
  94. $v8->executeString(file_get_contents($_SERVER['DOCUMENT_ROOT'] . '/js/marked.js'), 'marked.js');
  95. $this->post_data['content'] = $v8->executeString('marked(POST.content_md)');
  96. } catch (V8JsTimeLimitException $e) {
  97. die(json_encode(array('content_md' => '渲染超时')));
  98. } catch (V8JsException $e) {
  99. die(json_encode(array('content_md' => '未知错误')));
  100. }
  101. if (preg_match('/^.*<!--.*readmore.*-->.*$/m', $this->post_data['content'], $matches, PREG_OFFSET_CAPTURE)) {
  102. $content_less = substr($this->post_data['content'], 0, $matches[0][1]);
  103. $content_more = substr($this->post_data['content'], $matches[0][1] + strlen($matches[0][0]));
  104. $this->post_data['content'] = $purifier->purify($content_less).'<!-- readmore -->'.$purifier->purify($content_more);
  105. } else {
  106. $this->post_data['content'] = $purifier->purify($this->post_data['content']);
  107. }
  108. } elseif ($this->type == 'slide') {
  109. $content_array = yaml_parse($this->post_data['content_md']);
  110. if ($content_array === false || !is_array($content_array)) {
  111. die(json_encode(array('content_md' => '不合法的 YAML 格式')));
  112. }
  113. try {
  114. $v8 = new V8Js('PHP');
  115. $v8->executeString(file_get_contents($_SERVER['DOCUMENT_ROOT'] . '/js/marked.js'), 'marked.js');
  116. $v8->executeString(<<<EOD
  117. marked.setOptions({
  118. getLangClass: function(lang) {
  119. lang = lang.toLowerCase();
  120. switch (lang) {
  121. case 'c': return 'c';
  122. case 'c++': return 'cpp';
  123. case 'pascal': return 'pascal';
  124. default: return lang;
  125. }
  126. },
  127. getElementClass: function(tok) {
  128. switch (tok.type) {
  129. case 'list_item_start':
  130. return 'fragment';
  131. case 'loose_item_start':
  132. return 'fragment';
  133. default:
  134. return null;
  135. }
  136. }
  137. })
  138. EOD
  139. );
  140. } catch (V8JsException $e) {
  141. die(json_encode(array('content_md' => '未知错误')));
  142. }
  143. $marked = function($md) use ($v8, $purifier) {
  144. try {
  145. $v8->md = $md;
  146. return $purifier->purify($v8->executeString('marked(PHP.md)'));
  147. } catch (V8JsException $e) {
  148. die(json_encode(array('content_md' => '未知错误')));
  149. }
  150. };
  151. $config = array();
  152. $this->post_data['content'] = '';
  153. foreach ($content_array as $slide_name => $slide_content) {
  154. if (is_array($slide_content) && is_array($slide_content['config'])) {
  155. foreach (array('theme') as $config_key) {
  156. if (is_string($slide_content['config'][$config_key]) && strlen($slide_content['config'][$config_key]) <= 30) {
  157. $config[$config_key] = $slide_content['config'][$config_key];
  158. }
  159. }
  160. continue;
  161. }
  162. $this->post_data['content'] .= '<section>';
  163. if (is_string($slide_content)) {
  164. $this->post_data['content'] .= $marked($slide_content);
  165. } elseif (is_array($slide_content)) {
  166. if (is_array($slide_content['children'])) {
  167. foreach ($slide_content['children'] as $cslide_name => $cslide_content) {
  168. $this->post_data['content'] .= '<section>';
  169. $this->post_data['content'] .= $marked($cslide_content);
  170. $this->post_data['content'] .= '</section>';
  171. }
  172. }
  173. }
  174. $this->post_data['content'] .= "</section>\n";
  175. }
  176. $this->post_data['content'] = json_encode($config) . "\n" . $this->post_data['content'];
  177. }
  178. }
  179. public function handleSave() {
  180. $save = $this->save;
  181. $this->receivePostData();
  182. $ret = $save($this->post_data);
  183. if (!$ret) {
  184. $ret = array();
  185. }
  186. if (isset($_POST['need_preview'])) {
  187. ob_start();
  188. if ($this->type == 'blog') {
  189. echoUOJPageHeader('博客预览', array('ShowPageHeader' => false, 'REQUIRE_LIB' => array('mathjax' => '', 'hljs' => '')));
  190. echo '<article>';
  191. echo $this->post_data['content'];
  192. echo '</article>';
  193. echoUOJPageFooter(array('ShowPageFooter' => false));
  194. } elseif ($this->type == 'slide') {
  195. uojIncludeView('slide', array_merge(
  196. UOJContext::pageConfig(),
  197. array(
  198. 'PageTitle' => '幻灯片预览',
  199. 'content' => $this->post_data['content']
  200. )
  201. ));
  202. }
  203. $ret['html'] = ob_get_contents();
  204. ob_end_clean();
  205. }
  206. die(json_encode($ret));
  207. }
  208. public function runAtServer() {
  209. if (isset($_POST["save-{$this->name}"])) {
  210. $this->handleSave();
  211. }
  212. }
  213. public function printHTML() {
  214. uojIncludeView('blog-editor', array('editor' => $this));
  215. }
  216. }