diff --git a/web/app/libs/uoj-html-lib.php b/web/app/libs/uoj-html-lib.php
new file mode 100644
index 0000000..40cd787
--- /dev/null
+++ b/web/app/libs/uoj-html-lib.php
@@ -0,0 +1,18 @@
+content_md = $content_md;
+ $v8->executeString(file_get_contents($_SERVER['DOCUMENT_ROOT'] . '/js/marked.js'), 'marked.js');
+ $content = $v8->executeString('marked(PHP.content_md)');
+ } catch (V8JsException $e) {
+ throw new Exception('V8Js error: ' . $e->getMessage());
+ }
+
+ $content = $purifier->purify($content);
+
+ return $content;
+}
diff --git a/web/app/libs/uoj-luogu-lib.php b/web/app/libs/uoj-luogu-lib.php
new file mode 100644
index 0000000..220a074
--- /dev/null
+++ b/web/app/libs/uoj-luogu-lib.php
@@ -0,0 +1,75 @@
+ $sample) {
+ $display_sample_id = $id + 1;
+
+ $statement .= "\n#### 样例输入 #{$display_sample_id}\n\n";
+ $statement .= "\n```text\n{$sample[0]}\n```\n\n";
+
+ $statement .= "\n#### 样例输出 #{$display_sample_id}\n\n";
+ $statement .= "\n```text\n{$sample[1]}\n```\n\n";
+ }
+
+ $statement .= "\n### 说明/提示\n\n";
+ $statement .= $problem['hint'] . "\n";
+
+ return [
+ 'title' => "【洛谷 {$problem['pid']}】{$problem['title']}",
+ 'time_limit' => (float)max($problem['limits']['time']) / 1000.0,
+ 'memory_limit' => (float)max($problem['limits']['memory']) / 1024.0,
+ 'statement' => renderMarkdown($statement),
+ ];
+}
+
+function fetchLuoguProblemBasicInfo($pid) {
+ // ensure validateLuoguProblemId($pid) is true
+
+ $curl = Curl::init();
+
+ $curl->set('CURLOPT_HTTPHEADER', [
+ 'User-Agent: ' . LUOGU_USER_AGENT,
+ 'Content-Type: application/json',
+ 'Accept: application/json',
+ ]);
+
+ $curl->url(LUOGU_BASE_URL . '/problem/' . $pid . '?_contentOnly=1');
+
+ if ($curl->error()) {
+ throw new Exception('Curl error: ' . $curl->message());
+ }
+
+ $data = json_decode($curl->data(), true);
+
+ return parseLuoguProblemData($data['currentData']['problem']);
+}
diff --git a/web/app/libs/uoj-validate-lib.php b/web/app/libs/uoj-validate-lib.php
new file mode 100644
index 0000000..21cf3b0
--- /dev/null
+++ b/web/app/libs/uoj-validate-lib.php
@@ -0,0 +1,5 @@
+ 0,
+ 'CURLOPT_TIMEOUT' => 30,
+ 'CURLOPT_ENCODING' => '',
+ 'CURLOPT_IPRESOLVE' => 1,
+ 'CURLOPT_RETURNTRANSFER' => true,
+ 'CURLOPT_SSL_VERIFYPEER' => false,
+ 'CURLOPT_CONNECTTIMEOUT' => 10,
+ );
+
+ private $info;
+ private $data;
+ private $error;
+ private $message;
+
+ private static $instance;
+
+ /**
+ * Instance
+ * @return self
+ */
+ public static function init() {
+ if (self::$instance === null) {
+ self::$instance = new self;
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Task info
+ *
+ * @return array
+ */
+ public function info() {
+ return $this->info;
+ }
+
+ /**
+ * Result Data
+ *
+ * @return string
+ */
+ public function data() {
+ return $this->data;
+ }
+
+ /**
+ * Error status
+ *
+ * @return integer
+ */
+ public function error() {
+ return $this->error;
+ }
+
+ /**
+ * Error message
+ *
+ * @return string
+ */
+ public function message() {
+ return $this->message;
+ }
+
+ /**
+ * Set POST data
+ * @param array|string $data
+ * @param null|string $value
+ * @return self
+ */
+ public function post($data, $value = null) {
+ if (is_array($data)) {
+ foreach ($data as $key => $val) {
+ $this->post[$key] = $val;
+ }
+ } else {
+ if ($value === null) {
+ $this->post = $data;
+ } else {
+ $this->post[$data] = $value;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * File upload
+ * @param string $field
+ * @param string $path
+ * @param string $type
+ * @param string $name
+ * @return self
+ */
+ public function file($field, $path, $type, $name) {
+ $name = basename($name);
+
+ if (class_exists('CURLFile')) {
+ $this->set('CURLOPT_SAFE_UPLOAD', true);
+ $file = curl_file_create($path, $type, $name);
+ } else {
+ $file = "@{$path};type={$type};filename={$name}";
+ }
+
+ return $this->post($field, $file);
+ }
+
+ /**
+ * Save file
+ * @param string $path
+ * @return self
+ * @throws Exception
+ */
+ public function save($path) {
+ if ($this->error) {
+ throw new Exception($this->message, $this->error);
+ }
+
+ $fp = @fopen($path, 'w');
+
+ if ($fp === false) {
+ throw new Exception('Failed to save the content', 500);
+ }
+
+ fwrite($fp, $this->data);
+ fclose($fp);
+
+ return $this;
+ }
+
+ /**
+ * Request URL
+ * @param string $url
+ * @return self
+ * @throws Exception
+ */
+ public function url($url) {
+ if (filter_var($url, FILTER_VALIDATE_URL)) {
+ return $this->set('CURLOPT_URL', $url)->process();
+ }
+
+ throw new Exception('Target URL is required.', 500);
+ }
+
+ /**
+ * Set option
+ * @param array|string $item
+ * @param null|string $value
+ * @return self
+ */
+ public function set($item, $value = null) {
+ if (is_array($item)) {
+ foreach ($item as $key => $val) {
+ $this->custom[$key] = $val;
+ }
+ } else {
+ $this->custom[$item] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set retry times
+ * @param int $times
+ * @return self
+ */
+ public function retry($times = 0) {
+ $this->retry = $times;
+
+ return $this;
+ }
+
+ /**
+ * Task process
+ * @param int $retry
+ * @return self
+ */
+ private function process($retry = 0) {
+ $ch = curl_init();
+
+ $option = array_merge($this->option, $this->custom);
+
+ foreach ($option as $key => $val) {
+ if (is_string($key)) {
+ $key = constant(strtoupper($key));
+ }
+ curl_setopt($ch, $key, $val);
+ }
+
+ if ($this->post) {
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $this->convert($this->post));
+ }
+
+ $this->data = (string)curl_exec($ch);
+ $this->info = curl_getinfo($ch);
+ $this->error = curl_errno($ch);
+ $this->message = $this->error ? curl_error($ch) : '';
+
+ curl_close($ch);
+
+ if ($this->error && $retry < $this->retry) {
+ $this->process($retry + 1);
+ }
+
+ $this->post = array();
+ $this->retry = 0;
+
+ return $this;
+ }
+
+ /**
+ * Convert array
+ * @param array $input
+ * @param string $pre
+ * @return array
+ */
+ private function convert($input, $pre = null) {
+ if (is_array($input)) {
+ $output = array();
+
+ foreach ($input as $key => $value) {
+ $index = is_null($pre) ? $key : "{$pre}[{$key}]";
+ if (is_array($value)) {
+ $output = array_merge($output, $this->convert($value, $index));
+ } else {
+ $output[$index] = $value;
+ }
+ }
+
+ return $output;
+ }
+
+ return $input;
+ }
+}