Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN dpkg -s gnupg 2>/dev/null || (apt-get update && apt-get install -y gnupg) &&\
echo "deb http://ppa.launchpad.net/stesie/libv8/ubuntu bionic main" | tee /etc/apt/sources.list.d/stesie-libv8.list && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys D858A0DF &&\
apt-get update && apt-get install -y git vim ntp zip unzip curl wget apache2 libapache2-mod-xsendfile libapache2-mod-php php php-dev php-pear php-zip php-mysql php-mbstring php-gd php-intl php-xsl g++ make re2c libv8-7.5-dev libyaml-dev &&\
yes | pecl install yaml &&\
git clone https://github.com/phpv8/v8js.git --depth=1 -b 2.1.2 /tmp/pear/download/v8js-master && cd /tmp/pear/download/v8js-master &&\
phpize && ./configure --with-php-config=/usr/bin/php-config --with-v8js=/opt/libv8-7.5 && make install && cd -
yes | pecl install yaml

ADD . /opt/uoj
WORKDIR /opt/uoj
Expand Down
9 changes: 9 additions & 0 deletions web/app/models/HTML.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,13 @@ public static function pruifier() {

return new HTMLPurifier($config);
}

public static function parsedown($config = []) {
return new UOJMarkdown($config + [
'math' => [
'enabled' => true,
'matchSingleDollar' => true
]
]);
}
}
62 changes: 15 additions & 47 deletions web/app/models/UOJBlogEditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,12 @@ private function receivePostData() {
$this->post_data['is_hidden'] = isset($_POST["{$this->name}_is_hidden"]) ? 1 : 0;

$purifier = HTML::pruifier();
$parsedown = HTML::parsedown();

$this->post_data['title'] = HTML::escape($this->post_data['title']);

if ($this->type == 'blog') {
$content_md = $_POST[$this->name . '_content_md'];
try {
$v8 = new V8Js('POST');
$v8->content_md = $this->post_data['content_md'];
$v8->executeString(file_get_contents($_SERVER['DOCUMENT_ROOT'] . '/js/marked.js'), 'marked.js');
$this->post_data['content'] = $v8->executeString('marked(POST.content_md)');
} catch (V8JsException $e) {
die(json_encode(array('content_md' => '未知错误')));
}
$this->post_data['content'] = $parsedown->text($this->post_data['content_md']);

if (preg_match('/^.*<!--.*readmore.*-->.*$/m', $this->post_data['content'], $matches, PREG_OFFSET_CAPTURE)) {
$content_less = substr($this->post_data['content'], 0, $matches[0][1]);
Expand All @@ -118,45 +111,20 @@ private function receivePostData() {
if ($content_array === false || !is_array($content_array)) {
die(json_encode(array('content_md' => '不合法的 YAML 格式')));
}

try {
$v8 = new V8Js('PHP');
$v8->executeString(file_get_contents($_SERVER['DOCUMENT_ROOT'] . '/js/marked.js'), 'marked.js');
$v8->executeString(<<<EOD
marked.setOptions({
getLangClass: function(lang) {
lang = lang.toLowerCase();
switch (lang) {
case 'c': return 'c';
case 'c++': return 'cpp';
case 'pascal': return 'pascal';
default: return lang;
}
},
getElementClass: function(tok) {
switch (tok.type) {
case 'list_item_start':
return 'fragment';
case 'loose_item_start':
return 'fragment';
default:
return null;
}
}
})
EOD
);
} catch (V8JsException $e) {
die(json_encode(array('content_md' => '未知错误')));
}

$marked = function($md) use ($v8, $purifier) {
try {
$v8->md = $md;
return $purifier->purify($v8->executeString('marked(PHP.md)'));
} catch (V8JsException $e) {
die(json_encode(array('content_md' => '未知错误')));

$marked = function($md) use ($parsedown, $purifier) {
$dom = new DOMDocument;
$dom->loadHTML(mb_convert_encoding($parsedown->text($md), 'HTML-ENTITIES', 'UTF-8'));
$elements = $dom->getElementsByTagName('li');

foreach ($elements as $element) {
$element->setAttribute(
'class',
$element->getAttribute('class') . ' fragment'
Comment on lines +121 to +123
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will create a leading space in the class attribute if the element has no existing class, resulting in a class value like ' fragment'. Consider checking if the existing class is empty or trimming the result.

Suggested change
$element->setAttribute(
'class',
$element->getAttribute('class') . ' fragment'
$current_class = $element->getAttribute('class');
$element->setAttribute(
'class',
($current_class === '' ? 'fragment' : $current_class . ' fragment')

Copilot uses AI. Check for mistakes.
);
}

return $purifier->purify($dom->saveHTML());
};
Comment on lines +115 to 128
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loadHTML method may generate warnings for malformed HTML. Additionally, saveHTML() returns the entire HTML document including <html> and <body> tags, which is likely not desired. Consider using libxml_use_internal_errors(true) to suppress warnings and saveHTML($dom->documentElement) or extracting only the body content.

Copilot uses AI. Check for mistakes.

$config = array();
Expand Down
80 changes: 80 additions & 0 deletions web/app/models/UOJMarkdown.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

require_once __DIR__ . '../vendor/parsedown/ParsedownMath.php';
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path concatenation is incorrect. There's a missing directory separator between __DIR__ and the relative path. It should be __DIR__ . '/../vendor/parsedown/ParsedownMath.php'.

Suggested change
require_once __DIR__ . '../vendor/parsedown/ParsedownMath.php';
require_once __DIR__ . '/../vendor/parsedown/ParsedownMath.php';

Copilot uses AI. Check for mistakes.

class UOJMarkdown extends ParsedownMath {
public function __construct($options = '') {
if (method_exists(get_parent_class(), "__construct")) {
parent::__construct($options);
}

// https://gist.github.com/ShNURoK42/b5ce8baa570975db487c
$this->InlineTypes['@'][] = 'UserMention';
}

// https://github.com/taufik-nurrohman/parsedown-extra-plugin/blob/1653418c5a9cf5277cd28b0b23ba2d95d18e9bc4/ParsedownExtraPlugin.php#L340-L345
protected function doGetAttributes($Element) {
if (isset($Element['attributes'])) {
return (array) $Element['attributes'];
}
return array();
}

// https://github.com/taufik-nurrohman/parsedown-extra-plugin/blob/1653418c5a9cf5277cd28b0b23ba2d95d18e9bc4/ParsedownExtraPlugin.php#L347-L358
protected function doGetContent($Element) {
if (isset($Element['text'])) {
return $Element['text'];
}
if (isset($Element['rawHtml'])) {
return $Element['rawHtml'];
}
if (isset($Element['handler']['argument'])) {
return implode("\n", (array) $Element['handler']['argument']);
}
return null;
}

// https://github.com/taufik-nurrohman/parsedown-extra-plugin/blob/1653418c5a9cf5277cd28b0b23ba2d95d18e9bc4/ParsedownExtraPlugin.php#L369-L378
protected function doSetAttributes(&$Element, $From, $Args = array()) {
$Attributes = $this->doGetAttributes($Element);
$Content = $this->doGetContent($Element);
if (is_callable($From)) {
$Args = array_merge(array($Content, $Attributes, &$Element), $Args);
$Element['attributes'] = array_replace($Attributes, (array) call_user_func_array($From, $Args));
} else {
$Element['attributes'] = array_replace($Attributes, (array) $From);
}
}

// Add classes to <table>
protected function blockTableComplete($Block) {
$this->doSetAttributes($Block['element'], ['class' => 'table table-bordered']);

return $Block;
}

// https://gist.github.com/ShNURoK42/b5ce8baa570975db487c
protected function inlineUserMention($Excerpt) {
if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) {
$mentioned_user = queryUser($matches[1]);

if ($mentioned_user) {
return [
'extent' => strlen($matches[0]),
'element' => [
'name' => 'span',
'text' => '@' . $mentioned_user['username'],
'attributes' => [
'class' => "uoj-username",
],
],
];
}

return [
'extent' => strlen($matches[0]),
'markup' => $matches[0],
];
}
}
}
Loading