Merge branch 'master' into uoj_form_v2

This commit is contained in:
Baoshuo Ren 2023-01-15 21:27:12 +08:00
commit e2c3a3eb4e
Signed by: baoshuo
GPG Key ID: 00CB9680AB29F51A
160 changed files with 2305 additions and 431 deletions

45
.config.php Normal file
View File

@ -0,0 +1,45 @@
<?php
return [
'profile' => [
'oj-name' => '石家庄二中信息学在线评测系统',
'oj-name-short' => 'S2OJ',
'administrator' => 'root',
'admin-email' => 'admin@sjzezoj.com',
'QQ-group' => '',
'ICP-license' => '冀ICP备2020028886号',
],
'database' => [
'database' => 'app_uoj233',
'username' => 'root',
'password' => 'root',
'host' => 'uoj-db',
'port' => '3306',
],
'security' => [
'user' => [
'client_salt' => 'salt_0',
],
'cookie' => [
'checksum_salt' => ['salt_1', 'salt_2', 'salt_3'],
],
],
'mail' => [
'noreply' => [
'username' => 'noreply@local_uoj.ac',
'password' => '_mail_noreply_password_',
'host' => 'smtp.local_uoj.ac',
'secure' => 'tls',
'port' => 587,
]
],
'judger' => [
'socket' => [
'port' => '2333',
'password' => '_judger_socket_password_'
],
],
'switch' => [
'blog-domain-mode' => 3,
'open-register' => false,
],
];

3
.gitignore vendored
View File

@ -3,3 +3,6 @@ uoj_data_1/
uoj_data_2/
.php-cs-fixer.cache
docker-compose.local.yml
.config.php
.config.development.php
.config.local.php

View File

@ -979,6 +979,17 @@ CREATE TABLE `upgrades` (
LOCK TABLES `upgrades` WRITE;
/*!40000 ALTER TABLE `upgrades` DISABLE KEYS */;
INSERT INTO `upgrades` (`name`, `status`, `updated_at`) VALUES
('3_parsedown', 'up', now()),
('4_image_hosting', 'up', now()),
('6_user_info_v2', 'up', now()),
('8_group_v2', 'up', now()),
('9_list_v2', 'up', now()),
('14_sync_from_uoj.ac', 'up', now()),
('16_list_v3', 'up', now()),
('18_user_permissions', 'up', now()),
('20_problem_difficulty', 'up', now()),
('21_problem_difficulty', 'up', now());
/*!40000 ALTER TABLE `upgrades` ENABLE KEYS */;
UNLOCK TABLES;

View File

@ -60,15 +60,6 @@ services:
volumes:
- ./uoj_data/web/data:/var/uoj_data
- ./uoj_data/web/storage:/opt/uoj/web/app/storage
- ./.config.development.php:/opt/uoj/web/app/.config.php
ports:
- "80:80"
environment:
- UOJ_PROTOCOL=http
- DATABASE_HOST=uoj-db
- DATABASE_PASSWORD=root
- JUDGER_SOCKET_PORT=2333
- JUDGER_SOCKET_PASSWORD=_judger_socket_password_
- SALT_0=salt_0
- SALT_1=salt_1
- SALT_2=salt_2
- SALT_3=salt_3

View File

@ -43,15 +43,6 @@ services:
volumes:
- ./uoj_data/web/data:/var/uoj_data
- ./uoj_data/web/storage:/opt/uoj/web/app/storage
- ./.config.php:/opt/uoj/web/app/.config.php
ports:
- "80:80"
environment:
- UOJ_PROTOCOL=https
- DATABASE_HOST=uoj-db
- DATABASE_PASSWORD=root
- JUDGER_SOCKET_PORT=2333
- JUDGER_SOCKET_PASSWORD=_judger_socket_password_
- SALT_0=salt_0
- SALT_1=salt_1
- SALT_2=salt_2
- SALT_3=salt_3

View File

@ -20,21 +20,21 @@ return [
'domain' => null,
'main' => [
'protocol' => 'http',
'host' => '_httpHost_',
'port' => '80/443'
'host' => UOJContext::requestDomain(),
'port' => '80/443',
],
'blog' => [
'protocol' => 'http',
'host' => '_httpHost_',
'port' => '80/443'
'host' => UOJContext::requestDomain(),
'port' => '80/443',
]
],
'security' => [
'user' => [
'client_salt' => 'salt0'
'client_salt' => 'salt0',
],
'cookie' => [
'checksum_salt' => ['salt1', 'salt2', 'salt3']
'checksum_salt' => ['salt1', 'salt2', 'salt3'],
],
],
'mail' => [
@ -43,17 +43,17 @@ return [
'password' => '_mail_noreply_password_',
'host' => 'smtp.local_uoj.ac',
'secure' => 'tls',
'port' => 587
'port' => 587,
]
],
'judger' => [
'socket' => [
'port' => '233',
'password' => '_judger_socket_password_'
'password' => '_judger_socket_password_',
]
],
'switch' => [
'blog-domain-mode' => 3,
'open-register' => false
]
'open-register' => false,
],
];

View File

@ -79,19 +79,20 @@ $time_form->succ_href = "/contests";
$time_form->runAtServer();
?>
<?php echoUOJPageHeader('添加比赛') ?>
<?php echoUOJPageHeader(UOJLocale::get('contests::add new contest')) ?>
<div class="row">
<!-- left col -->
<div class="col-lg-9">
<div class="card card-default mb-2">
<div class="card-body">
<h1 class="card-title">添加比赛</h1>
<h1 class="card-title">
<?= UOJLocale::get('contests::add new contest') ?>
</h1>
<div class="w-full" style="max-width: 400px">
<?php $time_form->printHTML(); ?>
<?php $time_form->printHTML() ?>
</div>
</div>
</div>
</div>
@ -101,6 +102,7 @@ $time_form->runAtServer();
<aside class="col-lg-3 mt-3 mt-lg-0">
<?php uojIncludeView('sidebar') ?>
</aside>
<!-- end right col -->
</div>
<?php echoUOJPageFooter() ?>

View File

@ -0,0 +1,105 @@
<?php requireLib('bootstrap5') ?>
<?php echoUOJPageHeader(UOJLocale::get('html to markdown')) ?>
<h1>
<?= UOJLocale::get('html to markdown') ?>
</h1>
<style>
#html,
#markdown {
font-family: Cascadia Mono, Ubuntu Mono, Roboto Mono, Jetbrains Mono, Fira Code, Consolas, '思源黑体 Regular', '思源宋体 Light', '宋体', 'Courier New', monospace;
width: 100%;
min-height: 300px;
}
</style>
<div class="card">
<div class="card-body">
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<textarea class="form-control" id="html" placeholder="input html here"></textarea>
</div>
<div class="col">
<textarea data-no-autosize readonly class="form-control" id="markdown" placeholder="output markdown here" style="height: 100%"></textarea>
</div>
</div>
</div>
<div class="card-footer bg-transparent text-end">
<a href="https://s2oj.github.io/#/user/apps/html2markdown" target="_blank">使用教程</a>
</div>
</div>
<?= HTML::js_src('/js/turndown.js') ?>
<?= HTML::js_src('/js/turndown-plugin-gfm.js') ?>
<script>
function mathjaxScriptBlockType(node) {
if (node.nodeName !== 'SCRIPT') return null;
const a = node.getAttribute('type');
if (!a || a.indexOf('math/tex') < 0) return null;
return a.indexOf('display') >= 0 ? 'block' : 'inline';
}
var turndownService = new TurndownService({
headingStyle: 'atx',
hr: '---',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
fence: '```',
emDelimiter: '_',
strongDelimiter: '**',
linkStyle: 'inlined',
linkReferenceStyle: 'full',
preformattedCode: false,
});
turndownService.use(turndownPluginGfm.gfm);
turndownService.addRule('mathjaxRendered', {
filter: function(node) {
return node.nodeName === 'SPAN' && node.getAttribute('class') === 'MathJax';
},
replacement: function(content) {
return '';
}
});
turndownService.addRule('mathjaxScriptInline', {
filter: function(node) {
return mathjaxScriptBlockType(node) === 'inline';
},
escapeContent: function() {
// We want the raw unescaped content since this is what Katex will need to render
// If we escape, it will double the \\ in particular.
return false;
},
replacement: function(content, node, options) {
return '$' + content + '$';
}
});
turndownService.addRule('mathjaxScriptBlock', {
filter: function(node) {
return mathjaxScriptBlockType(node) === 'block';
},
escapeContent: function() {
return false;
},
replacement: function(content, node, options) {
return '$$\n' + content + '\n$$';
}
});
$(document).ready(function() {
$('#html').on('input', function() {
$('#markdown').val(turndownService.turndown($('#html').val()));
});
});
</script>
<?php echoUOJPageFooter() ?>

View File

@ -466,6 +466,10 @@ $pag = new Paginator($pag_config);
</div>
<script>
$(document).ready(function() {
[...document.querySelectorAll('[data-bs-toggle="tooltip"]')].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
});
var copy_url_toast = new bootstrap.Toast('#copy-url-toast', {
delay: 2000
});

View File

@ -1,59 +1,123 @@
<?php
requirePHPLib('form');
$forgot_form = new UOJBs4Form('forgot');
$forgot_form->addInput('username', 'text', '用户名', '',
function($username, &$vdata) {
if (!validateUsername($username)) {
return '用户名不合法';
}
$vdata['user'] = UOJUser::query($username);
if (!$vdata['user']) {
return '该用户不存在';
}
return '';
},
null
);
$forgot_form->handle = function(&$vdata) {
$user = $vdata['user'];
$password = $user["password"];
$oj_name = UOJConfig::$data['profile']['oj-name'];
$oj_name_short = UOJConfig::$data['profile']['oj-name-short'];
$sufs = base64url_encode($user['username'] . "." . md5($user['username'] . "+" . $password));
$url = HTML::url("/reset-password", array('params' => array('p' => $sufs)));
$html = <<<EOD
requirePHPLib('form');
use Gregwar\Captcha\PhraseBuilder;
$forgot_form = new UOJBs4Form('forgot');
$forgot_form->addInput(
'username',
'text',
'用户名',
'',
function ($username, &$vdata) {
if (!validateUsername($username)) {
return '用户名不合法';
}
$vdata['user'] = UOJUser::query($username);
if (!$vdata['user']) {
return '该用户不存在';
}
return '';
},
null
);
$forgot_form->appendHTML(<<<EOD
<div id="div-captcha" class="form-group">
<label for="input-captcha" class="col-sm-2 control-label">验证码</label>
<div class="col-sm-3" style="max-width: 60%">
<input type="text" class="form-control" id="input-captcha" name="captcha" placeholder="请输入验证码" maxlength="20" style="display: inline-block; width: 12em;" />
<div style="display: inline-block; margin-left: 8px; position: relative; top: -2px; cursor: pointer;">
<img id="captcha" src="" />
</div>
<span class="help-block" id="help-captcha" style="display: block"></span>
</div>
</div>
EOD);
$forgot_form->handle = function (&$vdata) {
$user = $vdata['user'];
$password = $user["password"];
if (!isset($_SESSION['phrase']) || !PhraseBuilder::comparePhrases($_SESSION['phrase'], $_POST['captcha'])) {
becomeMsgPage('验证码错误!');
}
if (!$user['email']) {
becomeMsgPage('用户未填写邮件地址,请联系管理员重置!');
}
$oj_name = UOJConfig::$data['profile']['oj-name'];
$oj_name_short = UOJConfig::$data['profile']['oj-name-short'];
$check_code = md5($user['username'] . "+" . $password . '+' . UOJTime::$time_now_str);
$sufs = base64url_encode($user['username'] . "." . $check_code);
$url = HTML::url("/reset-password", ['params' => ['p' => $sufs]]);
$oj_url = HTML::url('/');
$name = $user['username'];
$remote_addr = UOJContext::remoteAddr();
$http_x_forwarded_for = UOJContext::httpXForwardedFor();
$user_agent = UOJContext::httpUserAgent();
if ($user['realname']) {
$name .= ' (' . $user['realname'] . ')';
}
$html = <<<EOD
<base target="_blank" />
<p>{$user['username']}您好,</p>
<p>您刚刚启用了{$oj_name_short}密码找回功能,请进入下面的链接重设您的密码:</p>
<p><a href="$url">$url</a></p>
<p>{$oj_name}</p>
<p>{$name} 您好,</p>
<style type="text/css">
body{font-size:14px;font-family:arial,verdana,sans-serif;line-height:1.666;padding:0;margin:0;overflow:auto;white-space:normal;word-wrap:break-word;min-height:100px}
pre {white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}
</style>
<p>您最近告知我们需要重置您在 {$oj_name_short} 上账号的密码。请访问以下链接:<a href="{$url}">{$url}</a> (如果无法点击链接,请试着复制链接并粘贴至浏览器中打开。)</p>
<p>如果您没有请求重置密码,则忽略此信息。该链接将在 72 小时后自动过期失效。</p>
<ul>
<li><small>请求 IP: {$remote_addr} (转发来源: {$http_x_forwarded_for})</small></li>
<li><small>用户代理: {$user_agent}</small></li>
</ul>
<p>{$oj_name}</p>
<p><a href="{$oj_url}">{$oj_url}</a></p>
EOD;
$mailer = UOJMail::noreply();
$mailer->addAddress($user['email'], $user['username']);
$mailer->Subject = $oj_name_short."密码找回";
$mailer->msgHTML($html);
if (!$mailer->send()) {
error_log($mailer->ErrorInfo);
becomeMsgPage('<div class="text-center"><h2>邮件发送失败,请重试 <span class="glyphicon glyphicon-remove"></span></h2></div>');
} else {
becomeMsgPage('<div class="text-center"><h2>邮件发送成功 <span class="glyphicon glyphicon-ok"></span></h2></div>');
}
};
$forgot_form->submit_button_config['align'] = 'offset';
$forgot_form->runAtServer();
?>
$mailer = UOJMail::noreply();
$mailer->addAddress($user['email'], $user['username']);
$mailer->Subject = $oj_name_short . " 密码找回";
$mailer->msgHTML($html);
if (!$mailer->send()) {
error_log($mailer->ErrorInfo);
becomeMsgPage('<div class="text-center"><h2>邮件发送失败,请重试!</h2></div>');
} else {
DB::update([
"update user_info",
"set", [
'extra' => DB::json_set('extra', '$.reset_password_check_code', $check_code, '$.reset_password_time', UOJTime::$time_now_str),
],
"where", [
"username" => $user['username'],
],
]);
becomeMsgPage('<div class="text-center"><h2>邮件发送成功,请检查收件箱!</h2><span>如果邮件未出现在收件箱中,请检查垃圾箱。</span></div>');
}
};
$forgot_form->submit_button_config['align'] = 'offset';
$forgot_form->runAtServer();
?>
<?php echoUOJPageHeader('找回密码') ?>
<h2 class="page-header">找回密码</h2>
<h4>请输入需要找回密码的用户名:</h4>
<?php $forgot_form->printHTML(); ?>
<script>
function refreshCaptcha() {
var timestamp = new Date().getTime();
$("#captcha").attr("src", "/captcha" + '?' + timestamp);
}
$(document).ready(function() {
refreshCaptcha();
$("#captcha").click(function(e) {
refreshCaptcha();
});
});
</script>
<?php echoUOJPageFooter() ?>

View File

@ -49,7 +49,7 @@ UOJGroup::cur()->userCanView(Auth::user(), ['ensure' => true]);
</h2>
<?php if (UOJGroup::info('announcement')) : ?>
<div class="text-break">
<?= HTML::purifier_inline()->purify(HTML::parsedown()->line(UOJGroup::info('announcement'))) ?>
<?= HTML::purifier_inline()->purify(HTML::parsedown(['username_with_color' => true])->line(UOJGroup::info('announcement'))) ?>
</div>
<?php else : ?>
<div class="text-muted">

View File

@ -1,46 +0,0 @@
<?php requireLib('bootstrap5') ?>
<?php echoUOJPageHeader(UOJLocale::get('html to markdown')) ?>
<h1>
<?= UOJLocale::get('html to markdown') ?>
</h1>
<style>
#html,
#markdown {
font-family: Cascadia Mono, Ubuntu Mono, Roboto Mono, Jetbrains Mono, Fira Code, Consolas, '思源黑体 Regular', '思源宋体 Light', '宋体', 'Courier New', monospace;
width: 100%;
min-height: 300px;
}
</style>
<div class="card">
<div class="card-body">
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<textarea class="form-control" id="html" placeholder="input html here"></textarea>
</div>
<div class="col">
<textarea data-no-autosize readonly class="form-control" id="markdown" placeholder="output markdown here" style="height: 100%"></textarea>
</div>
</div>
</div>
<div class="card-footer bg-transparent text-end">
<a href="https://s2oj.github.io/#/user/apps/html2markdown" target="_blank">使用教程</a>
</div>
</div>
<?= HTML::js_src('/js/h2m.js') ?>
<script>
$(document).ready(function() {
$('#html').on('input', function() {
$('#markdown').val(h2m($('#html').val(), {
converter: 'Gfm'
}));
});
});
</script>
<?php echoUOJPageFooter() ?>

View File

@ -318,7 +318,7 @@ EOD);
echo HTML::tag_begin('tr');
echo HTML::tag('td', ['class' => 'text-center'], $problem->info['id']);
echo HTML::tag_begin('td');
echo $problem->getLink();
echo $problem->getLink(['with' => 'none']);
if ($problem->info['is_hidden']) {
echo ' <span class="badge text-bg-danger"><i class="bi bi-eye-slash-fill"></i> ', UOJLocale::get('hidden'), '</span> ';
}

View File

@ -9,16 +9,28 @@ Auth::check() || redirectToLogin();
UOJUser::checkPermission(Auth::user(), 'lists.view') || UOJResponse::page403();
if (UOJList::userCanCreateList(Auth::user())) {
$new_list_form = new UOJBs4Form('new_list');
$new_list_form = new UOJForm('new_list');
$new_list_form->handle = function () {
DB::insert("insert into lists (title, is_hidden) values ('未命名题单', 1)");
$id = DB::insert_id();
DB::insert("insert into lists_contents (id, content, content_md) values ($id, '', '')");
DB::insert([
"insert into lists",
DB::bracketed_fields(['title', 'is_hidden']),
"values",
DB::tuple(['未命名题单', 1]),
]);
$list_id = DB::insert_id();
DB::insert([
"insert into lists_contents",
DB::bracketed_fields(['id', 'content', 'content_md']),
"values",
DB::tuple([$list_id, '', '']),
]);
redirectTo("/list/{$list_id}");
die();
};
$new_list_form->submit_button_config['align'] = 'right';
$new_list_form->submit_button_config['class_str'] = 'btn btn-primary';
$new_list_form->submit_button_config['text'] = UOJLocale::get('problems::add new list');
$new_list_form->submit_button_config['smart_confirm'] = '';
$new_list_form->config['submit_container']['class'] = 'text-end';
$new_list_form->config['submit_button']['class'] = 'btn btn-primary';
$new_list_form->config['submit_button']['text'] = UOJLocale::get('problems::add new list');
$new_list_form->config['confirm']['smart'] = true;
$new_list_form->runAtServer();
}

View File

@ -100,7 +100,7 @@ if (UOJContest::cur()) {
$submission_requirement = UOJProblem::cur()->getSubmissionRequirement();
$custom_test_requirement = UOJProblem::cur()->getCustomTestRequirement();
$custom_test_enabled = false; // $custom_test_requirement && $pre_submit_check_ret === true;
$custom_test_enabled = $custom_test_requirement && $pre_submit_check_ret === true;
function handleUpload($zip_file_name, $content, $tot_size) {
global $is_participating;
@ -193,7 +193,7 @@ if ($pre_submit_check_ret === true && !$no_more_submission) {
'FS::randomAvailableSubmissionFileName',
'handleUpload'
);
$zip_answer_form->extra_validators[] = $submission_extra_validator;
$zip_answer_form->extra_validator = $submission_extra_validator;
$zip_answer_form->succ_href = $is_participating ? '/contest/' . UOJContest::info('id') . '/submissions' : '/submissions';
$zip_answer_form->runAtServer();
}
@ -385,46 +385,64 @@ if (UOJContest::cur()) {
<div class="card mb-2">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">上传者</span>
<span><?= UOJProblem::cur()->getUploaderLink() ?></span>
<span class="flex-shrink-0">
<?= UOJLocale::get('problems::uploader') ?>
</span>
<span>
<?= UOJProblem::cur()->getUploaderLink() ?>
</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">难度</span>
<span><?= UOJProblem::cur()->getDifficultyHTML() ?></span>
</li>
<?php if (Auth::check()) : ?>
<?php if (!UOJContest::cur() || UOJContest::cur()->progress() >= CONTEST_FINISHED) : ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">历史分数</span>
<?php $his_score = DB::selectSingle(["select max(score)", "from submissions", "where", ["problem_id" => UOJProblem::info('id'), "submitter" => Auth::id()]]) ?>
<span class="flex-shrink-0">
<?= UOJLocale::get('problems::difficulty') ?>
</span>
<span>
<?= UOJProblem::cur()->getDifficultyHTML() ?>
</span>
</li>
<?php if (Auth::check()) : ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">
<?= UOJLocale::get('problems::historical score') ?>
</span>
<?php $his_score = DB::selectSingle(["select max(score)", "from submissions", "where", ["problem_id" => UOJProblem::info('id'), "submitter" => Auth::id()]]) ?>
<a class="<?= is_null($his_score) ? '' : 'uoj-score' ?>" href="<?= HTML::url('/submissions', ['params' => ['problem_id' => UOJProblem::info('id'), 'submitter' => Auth::id()]]) ?>">
<?= is_null($his_score) ? '无' : $his_score ?>
</a>
<a class="<?= is_null($his_score) ? '' : 'uoj-score' ?>" href="<?= HTML::url('/submissions', ['params' => ['problem_id' => UOJProblem::info('id'), 'submitter' => Auth::id()]]) ?>">
<?= is_null($his_score) ? '无' : $his_score ?>
</a>
</li>
<?php endif ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">
<?= UOJLocale::get('problems::tags') ?>
</span>
<span>
<?php if (UOJProblem::info('is_hidden')) : ?>
<a href="<?= HTML::url('/problems', ['params' => ['is_hidden' => 'on']]) ?>">
<span class="badge text-bg-danger">
<i class="bi bi-eye-slash-fill"></i>
<?= UOJLocale::get('hidden') ?>
</span>
</a>
<?php endif ?>
<?php foreach (UOJProblem::cur()->queryTags() as $tag) : ?>
<?= HTML::tag(
'a',
['class' => 'uoj-problem-tag'],
HTML::tag('span', ['class' => 'badge bg-secondary'], HTML::escape($tag))
) ?>
<?php endforeach ?>
</span>
</li>
<?php endif ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">标签</span>
<span>
<?php if (UOJProblem::info('is_hidden')) : ?>
<a href="<?= HTML::url('/problems', ['params' => ['is_hidden' => 'on']]) ?>">
<span class="badge text-bg-danger">
<i class="bi bi-eye-slash-fill"></i>
<?= UOJLocale::get('hidden') ?>
</span>
</a>
<?php endif ?>
<?php foreach (UOJProblem::cur()->queryTags() as $tag) : ?>
<?= HTML::tag(
'a',
['class' => 'uoj-problem-tag'],
HTML::tag('span', ['class' => 'badge bg-secondary'], HTML::escape($tag))
) ?>
<?php endforeach ?>
<span class="flex-shrink-0">
<?= UOJLocale::get('appraisal') ?>
</span>
<span>
<?= UOJProblem::cur()->getZanBlock() ?>
</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">评价</span>
<span><?= UOJProblem::cur()->getZanBlock() ?></span>
</li>
</ul>
</div>

View File

@ -73,6 +73,9 @@ EOD;
])
]);
dataNewProblem($id);
redirectTo("/problem/{$id}/manage/statement");
die();
};
$new_problem_form->submit_button_config['align'] = 'right';
$new_problem_form->submit_button_config['class_str'] = 'btn btn-primary';

View File

@ -101,6 +101,29 @@ if (UOJProblem::cur()->userCanManage(Auth::user()) || UOJProblem::cur()->userPer
]);
};
$add_new_solution_form->runAtServer();
if (UOJUser::checkPermission(Auth::user(), 'blogs.create')) {
$quick_add_new_solution_form = new UOJForm('quick_add_new_solution');
$quick_add_new_solution_form->config['submit_container']['class'] = '';
$quick_add_new_solution_form->config['submit_button']['class'] = 'btn btn-link text-decoration-none p-0';
$quick_add_new_solution_form->config['submit_button']['text'] = '快速新建文章';
$quick_add_new_solution_form->handle = function () {
DB::insert([
"insert into blogs",
"(title, content, content_md, poster, is_hidden, post_time, active_time)",
"values", DB::tuple([
'【题解】' . UOJProblem::cur()->getTitle(), '', '',
Auth::id(), false, DB::now(), DB::now()
])
]);
$blog_id = DB::insert_id();
redirectTo(HTML::blog_url(Auth::id(), "/post/{$blog_id}/write"));
die();
};
$quick_add_new_solution_form->runAtServer();
}
}
$pag_config = [
@ -262,18 +285,16 @@ $pag = new Paginator($pag_config);
您当前无法为本题新增题解。
<?php endif ?>
</div>
<div class="card-footer bg-transparent">
<a target="_blank" class="text-decoration-none" href="<?= HTML::blog_url(Auth::id(), '/post/new/write?title=' . urlencode('【题解】#' . UOJProblem::info('id') . '. ' . UOJProblem::info('title')) . '&is_hidden=0') ?>">
快速新建文章
</a>
<div class="small text-muted mt-1">发布文章后,请返回本页输入博客 ID</div>
</div>
<?php if (isset($quick_add_new_solution_form)) : ?>
<div class="card-footer bg-transparent">
<?php $quick_add_new_solution_form->printHTML() ?>
</div>
<?php endif ?>
</div>
<?php uojIncludeView('sidebar'); ?>
<!-- End right col -->
<?php uojIncludeView('sidebar') ?>
</aside>
<!-- end right col -->
</div>
<?php echoUOJPageFooter() ?>

View File

@ -174,6 +174,85 @@ $difficulty_form->runAtServer();
</ul>
</div>
<div class="card mt-3">
<div class="card-header fw-bold">
标签填充
</div>
<div class="card-body">
<script>
function fillTag(tags) {
if (typeof tags === 'string') tags = [tags];
tags = tags.map(tag => tag.trim()).filter(Boolean);
var originalTags = $('#input-problem_tags')
.val()
.replace(//g, ',')
.split(',')
.map(tag => tag.trim())
.filter(Boolean);
var newTagsSet = new Set(originalTags.concat(tags));
$('#input-problem_tags').val(Array.from(newTagsSet.values()).join(', '));
$('#input-problem_tags').trigger('input');
}
</script>
<div class="row row-cols-4 row-cols-lg-2 g-2">
<?php foreach (UOJProblem::$categories as $category => $tags) : ?>
<?php $category_id = uniqid('category-'); ?>
<div class="d-inline-block" id="category-container-<?= $category_id ?>">
<button id="category-button-<?= $category_id ?>" class="btn btn-sm btn-light w-100" type="button"><?= $category ?></button>
</div>
<script>
$(document).ready(function() {
bootstrap.Popover.jQueryInterface.call($('#category-button-<?= $category_id ?>'), {
container: $('#category-container-<?= $category_id ?>'),
html: true,
placement: 'left',
animation: false,
trigger: 'manual',
fallbackPlacements: ['bottom', 'right'],
content: [
<?php foreach ($tags as $tag) : ?> '<?= $tag ?>', <?php endforeach ?>
].map(tag => ('<button class="btn btn-sm btn-light d-inline-block mr-1 mb-1" onclick="fillTag([\'<?= $category ?>\', \'' + tag + '\'])">' + tag + '</button>')).join(' '),
sanitizeFn(content) {
return content;
},
}).on("mouseenter", function() {
var _this = this;
$(this).popover("show");
$(this).siblings(".popover").on("mouseleave", function() {
$(_this).popover('hide');
});
}).on("mouseleave", function() {
var _this = this;
var check_popover_status = function() {
setTimeout(function() {
if (!$(".popover:hover").length) {
$(_this).popover("hide")
} else {
check_popover_status();
}
}, 50);
};
check_popover_status();
});
});
</script>
<?php endforeach ?>
</div>
</div>
<div class="card-footer text-muted small bg-transparent">
将鼠标悬浮至主分类上,点击弹出框中的对应标签即可将其填充至题目标签中。
</div>
</div>
<div class="card mt-3">
<div class="card-header fw-bold">
题目难度
@ -183,7 +262,6 @@ $difficulty_form->runAtServer();
</div>
</div>
</aside>
</div>
<?php echoUOJPageFooter() ?>

View File

@ -1,39 +1,55 @@
<?php
if (!isset($_GET['p'])) {
become404Page();
if (!isset($_GET['p'])) {
become404Page();
}
list($username, $check_code) = explode('.', base64url_decode($_GET['p']));
$user = UOJUser::query($username);
if (!$user) become404Page();
if (!isset($check_code) || strlen($check_code) != 32) become404Page();
$extra = UOJUser::getExtra($user);
if ($check_code !== $extra['reset_password_check_code']) {
become404Page();
}
if (UOJTime::str2time($extra['reset_password_time'])->add(new DateInterval('P3D')) < UOJTime::$time_now) {
becomeMsgPage('链接已过期');
}
function resetPassword() {
global $user;
if (!isset($_POST['newPW']) || !validatePassword($_POST['newPW'])) {
return '操作失败,无效密码';
}
function resetPassword() {
list($username, $check_code) = explode('.', base64url_decode($_GET['p']));
if (!isset($_POST['newPW']) || !validatePassword($_POST['newPW'])) {
return '操作失败,无效密码';
}
if (!isset($username) || !validateUsername($username)) {
return '不明错误';
}
if (!isset($check_code)) {
return '不明错误';
}
$newPW = $_POST['newPW'];
$user = UOJUser::query($username);
if ($user == null) {
return '不明错误';
}
if ($check_code !== md5($user['username'] . '+' . $user['password'])) {
return '不明错误';
}
$newPW = getPasswordToStore($newPW, $user['username']);
DB::update("update user_info set password = '$newPW' where username = '{$user['username']}'");
return 'ok';
}
if (isset($_POST['reset'])) {
die(resetPassword());
}
?>
$newPW = $_POST['newPW'];
$newPW = getPasswordToStore($newPW, $user['username']);
DB::update([
"update user_info",
"set", [
"password" => $newPW,
"extra" => DB::json_remove('extra', '$.reset_password_check_code', '$.reset_password_time'),
],
"where", [
"username" => $user['username'],
],
]);
return 'ok';
}
if (isset($_POST['reset'])) {
die(resetPassword());
}
?>
<?php
$REQUIRE_LIB['dialog'] = '';
$REQUIRE_LIB['md5'] = '';
?>
$REQUIRE_LIB['dialog'] = '';
$REQUIRE_LIB['md5'] = '';
?>
<?php echoUOJPageHeader('更改密码') ?>
<h2 class="page-header">更改密码</h2>
<form id="form-reset" class="form-horizontal">
@ -44,60 +60,60 @@
<input type="password" class="form-control top-buffer-sm" id="input-confirm_password" placeholder="再次输入新密码" maxlength="20" />
<span class="help-block" id="help-password"></span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-3">
<button type="submit" id="button-submit" class="btn btn-secondary">提交</button>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-3">
<button type="submit" id="button-submit" class="btn btn-secondary">提交</button>
</div>
</div>
</form>
<script type="text/javascript">
function validateResetPwPost() {
var ok = true;
ok &= getFormErrorAndShowHelp('password', validateSettingPassword);
return ok;
}
$(document).ready(function() {
$('#form-reset').submit(function(e) {
if (!validateResetPwPost()) {
return false;
}
$.post(<?= json_encode($_SERVER['REQUEST_URI']) ?>, {
reset : '',
newPW : md5($('#input-password').val(), "<?= getPasswordClientSalt() ?>")
}, function(res) {
if (res == 'ok') {
BootstrapDialog.show({
title : '提示',
message : '密码更改成功',
type : BootstrapDialog.TYPE_SUCCESS,
buttons: [{
label: '好的',
action: function(dialog) {
dialog.close();
}
}],
onhidden : function(dialog) {
window.location.href = '/login';
}
});
} else {
BootstrapDialog.show({
title : '提示',
message : res,
type : BootstrapDialog.TYPE_DANGER,
buttons: [{
function validateResetPwPost() {
var ok = true;
ok &= getFormErrorAndShowHelp('password', validateSettingPassword);
return ok;
}
$(document).ready(function() {
$('#form-reset').submit(function(e) {
if (!validateResetPwPost()) {
return false;
}
$.post(<?= json_encode($_SERVER['REQUEST_URI']) ?>, {
reset: '',
newPW: md5($('#input-password').val(), "<?= getPasswordClientSalt() ?>")
}, function(res) {
if (res == 'ok') {
BootstrapDialog.show({
title: '提示',
message: '密码更改成功',
type: BootstrapDialog.TYPE_SUCCESS,
buttons: [{
label: '好的',
action: function(dialog) {
dialog.close();
}
}]
});
}
}],
onhidden: function(dialog) {
window.location.href = '/login';
}
});
} else {
BootstrapDialog.show({
title: '提示',
message: res,
type: BootstrapDialog.TYPE_DANGER,
buttons: [{
label: '好的',
action: function(dialog) {
dialog.close();
}
}]
});
}
});
return false;
});
return false;
});
});
</script>
<?php echoUOJPageFooter() ?>

View File

@ -201,7 +201,9 @@ $comments_pag = new Paginator([
"order by id"
]);
foreach ($replies as $idx => $reply) {
$replies[$idx]['poster_realname'] = UOJUser::query($reply['poster'])['realname'];
$reply_user = UOJUser::query($reply['poster']);
$replies[$idx]['poster_realname'] = $reply_user['realname'];
$replies[$idx]['poster_username_color'] = UOJUser::getUserColor($reply_user);
$replies[$idx]['content'] = getCommentContentToDisplay($reply);
}
$replies_json = json_encode($replies);

View File

@ -32,7 +32,7 @@ $header_row .= '<th style="width:35em">' . UOJLocale::get('contests::problem sel
$header_row .= '<th style="width:35em">' . UOJLocale::get('contests::contest self review') . '</th>';
$header_row .= '</tr>';
$parsedown = HTML::parsedown();
$parsedown = HTML::parsedown(['username_with_color' => true]);
$purifier = HTML::purifier_inline();
$print_row = function ($row) use ($parsedown, $purifier) {

View File

@ -168,7 +168,9 @@ if ($perm['manager_view']) {
?>
<?php if ($perm['content'] || $perm['manager_view']) : ?>
<?php UOJSubmission::cur()->echoContent() ?>
<div class="copy-button-container">
<?php UOJSubmission::cur()->echoContent() ?>
</div>
<?php if (isset($hack_form)) : ?>
<p class="text-center">
@ -221,17 +223,17 @@ if (UOJSubmission::cur()->hasJudged()) {
?>
<div class="d-flex gap-2 justify-content-end">
<?php if (isset($minor_rejudge_form)) : ?>
<?php $minor_rejudge_form->printHTML() ?>
<?php endif ?>
<?php if (isset($minor_rejudge_form)) : ?>
<?php $minor_rejudge_form->printHTML() ?>
<?php endif ?>
<?php if (isset($rejudge_form)) : ?>
<?php $rejudge_form->printHTML() ?>
<?php endif ?>
<?php if (isset($rejudge_form)) : ?>
<?php $rejudge_form->printHTML() ?>
<?php endif ?>
<?php if (isset($delete_form)) : ?>
<?php $delete_form->printHTML() ?>
<?php endif ?>
<?php if (isset($delete_form)) : ?>
<?php $delete_form->printHTML() ?>
<?php endif ?>
</div>
<?php echoUOJPageFooter() ?>

View File

@ -516,7 +516,7 @@ EOD);
'new_tmp_expiration_time',
'text',
'过期时间',
(new DateTime())->add(new DateInterval('P7D'))->format('Y-m-d H:i:s'),
UOJTime::time2str((new DateTime())->add(new DateInterval('P7D'))->setTime(0, 0, 0)),
function ($str, &$vdata) {
try {
$vdata['expiration_time'] = new DateTime($str);
@ -657,6 +657,7 @@ EOD);
);
$change_usergroup_form->addVSelect('op_type', [
'banneduser' => '设为封禁用户',
'tempuser' => '设为临时用户',
'normaluser' => '设为普通用户',
'superuser' => '设为超级用户',
], '操作类型', '');
@ -666,15 +667,59 @@ EOD);
switch ($_POST['op_type']) {
case 'banneduser':
DB::update("update user_info set usergroup = 'B', usertype = 'banned' where username = '{$username}'");
$usergroup = '被封禁的用户';
DB::update([
"update user_info",
"set", [
"usergroup" => "B",
"usertype" => "banned",
"expiration_time" => null,
],
"where", [
"username" => $username,
],
]);
$usergroup = '被封禁的用户,该用户将无法再次登录系统';
break;
case 'tempuser':
DB::update([
"update user_info",
"set", [
"usergroup" => "T",
"usertype" => "student",
"expiration_time" => DB::now(),
],
"where", [
"username" => $username,
],
]);
$usergroup = '临时用户,请前往个人信息编辑页面修改过期时间';
break;
case 'normaluser':
DB::update("update user_info set usergroup = 'U', usertype = 'student' where username = '{$username}'");
DB::update([
"update user_info",
"set", [
"usergroup" => "U",
"usertype" => "student",
"expiration_time" => null,
],
"where", [
"username" => $username,
],
]);
$usergroup = '普通用户';
break;
case 'superuser':
DB::update("update user_info set usergroup = 'S', usertype = 'student' where username = '{$username}'");
DB::update([
"update user_info",
"set", [
"usergroup" => "S",
"usertype" => "student",
"expiration_time" => null,
],
"where", [
"username" => $username,
],
]);
$usergroup = '超级用户';
break;
}
@ -1336,7 +1381,7 @@ EOD);
EOD,
function ($row) {
echo '<tr>';
echo '<td>', '<span class="uoj-username" data-realname="', HTML::escape($row['realname']), '">', $row['username'], '</span>', '</td>';
echo '<td>', UOJUser::getLink($row), '</td>';
echo '<td>', HTML::escape($row['school']), '</td>';
echo '<td>';
switch ($row['usergroup']) {
@ -1429,7 +1474,8 @@ EOD);
<h5>注意事项</h5>
<ul class="mb-0">
<li>用户被封禁后将不能再次登录系统。</li>
<li>将当前用户移除权限后将无法再次访问本页面。</li>
<li>将用户修改为临时用户后,请前往个人信息编辑页面修改过期时间。</li>
<li>将当前用户移除管理权限后将无法再次访问本页面。</li>
<li>在修改用户类别前请仔细核对用户名以免产生不必要的麻烦。</li>
<li>如需为用户设置题目上传者、题目管理员等权限,请前往对应用户的个人资料编辑页面,点击「特权」选项卡修改。</li>
</ul>

View File

@ -266,6 +266,43 @@ EOD);
},
]
);
if ($user['usergroup'] == 'B') {
$update_profile_form->appendHTML(<<<EOD
<div class="mb-3">
<label for="input-username_color" class="form-label">用户名颜色</label>
<input type="text" class="form-control" id="input-username_color" aria-describedby="help-username_color" value="棕色 - #996600" disabled>
<div id="help-username_color" class="form-text">被封禁的用户无法修改用户名颜色。</div>
</div>
EOD);
} else if ($user['usergroup'] == 'T') {
$update_profile_form->appendHTML(<<<EOD
<div class="mb-3">
<label for="input-username_color" class="form-label">用户名颜色</label>
<input type="text" class="form-control" id="input-username_color" aria-describedby="help-username_color" value="灰色 - #707070" disabled>
<div id="help-username_color" class="form-text">临时用户无法修改用户名颜色。</div>
</div>
EOD);
} else {
$additional_colors = [];
if (isSuperUser($user)) {
$additional_colors['#9d3dcf'] = '紫色 - #9d3dcf';
}
$update_profile_form->addSelect('username_color', [
'div_class' => 'mb-3',
'label' => '用户名颜色',
'default_value' => $extra['username_color'],
'options' => $additional_colors + [
'#0d6efd' => '蓝色 - #0d6efd',
'#2da44e' => '绿色 - #2da44e',
'#e85aad' => '粉色 - #e85aad',
'#f32a38' => '红色 - #f32a38',
'#f57c00' => '橙色 - #f57c00',
'#00acc1' => '青色 - #00acc1',
],
]);
}
$update_profile_form->handle = function (&$vdata) use ($user) {
$data = [
'email' => $vdata['email'],
@ -301,7 +338,9 @@ EOD);
'$.social.codeforces',
$vdata['codeforces'],
'$.social.website',
$vdata['website']
$vdata['website'],
'$.username_color',
$_POST['username_color']
),
],
"where", ["username" => $user['username']]
@ -376,6 +415,17 @@ EOD);
}
$update_user_permissions_form->appendHTML(HTML::tag('span', [], UOJLocale::get('user::user group')));
$update_user_permissions_form->appendHTML(HTML::tag('span', ['class' => 'd-inline-block ms-3'], $type_text));
$update_user_permissions_form->addSelect('user_type', [
'label' => '账号类型',
'options' => [
'student' => '学生',
'teacher' => '老师',
'system' => '系统',
],
'div_class' => 'my-3 row gy-2 gx-3 align-items-center',
'label_class' => 'form-label col-auto',
'select_class' => 'form-select w-auto col-auto',
]);
$update_user_permissions_form->appendHTML(HTML::tag('h3', ['class' => 'h5 mt-3'], '题目'));
$update_user_permissions_form->addCheckbox('problems__view', [
'checked' => $extra['permissions']['problems']['view'],
@ -707,6 +757,7 @@ EOD);
DB::update([
"update user_info",
"set", [
"usertype" => $_POST['user_type'],
"extra" => json_encode($extra),
],
"where", [

View File

@ -87,14 +87,23 @@ function queryContestData($contest, $config = []) {
$people = [];
if ($contest['extra_config']['individual_or_team'] == 'individual') {
$people = DB::selectAll([
"select contests_registrants.username, user_info.realname from contests_registrants",
$res = DB::selectAll([
"select contests_registrants.username, user_info.realname, user_info.extra, user_info.usergroup from contests_registrants",
"inner join user_info on contests_registrants.username = user_info.username",
"where", [
"contest_id" => $contest['id'],
"has_participated" => 1
]
], DB::NUM);
foreach ($res as $row) {
$extra = json_decode($row[2], true);
$people[] = [
$row[0],
trim(HTML::escape($row[1])),
null,
UOJUser::getUserColor2($row[3], $extra['username_color']),
];
}
} elseif ($contest['extra_config']['individual_or_team'] == 'team') {
$res = DB::selectAll([
"select user_info.username, null, user_info.extra from contests_registrants, user_info",
@ -106,11 +115,15 @@ function queryContestData($contest, $config = []) {
], DB::NUM);
foreach ($res as $row) {
$extra = json_decode($row[2], true);
$row[2] = [
'team_name' => $extra['acm']['team_name'],
'members' => $extra['acm']['members']
$people[] = [
$row[0],
null,
[
'team_name' => $extra['acm']['team_name'],
'members' => $extra['acm']['members'],
],
null,
];
$people[] = $row;
}
}
@ -373,7 +386,7 @@ function calcStandings($contest, $contest_data, &$score, &$standings, $cfg = [])
}
}
// standings: rank => score, penalty, [username, realname], virtual_rank, ?review
// standings: rank => score, penalty, [username, realname, null|array, null|color], virtual_rank, ?review
$standings = [];
foreach ($contest_data['people'] as $person) {
$cur = array(0, 0, $person);

View File

@ -735,7 +735,7 @@ function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle
$stat = $zip_file->statName($req['file_name']);
if ($req['type'] == 'source code') {
$max_size = isset($req['size']) ? (int)$req['size'] : 50;
$max_size = isset($req['size']) ? (int)$req['size'] : 100;
if ($stat['size'] > $max_size * 1024) {
$zip_file->close();
unlink(UOJContext::storagePath() . $zip_file_name);

View File

@ -1009,62 +1009,6 @@ function echoUOJPageFooter($config = array()) {
uojIncludeView('page-footer', $config);
}
function echoRanklist($config = []) {
$header_row = '';
$header_row .= '<tr>';
$header_row .= '<th style="width: 5em;">#</th>';
$header_row .= '<th style="width: 14em;">' . UOJLocale::get('username') . '</th>';
$header_row .= '<th style="width: 50em;">' . UOJLocale::get('motto') . '</th>';
$header_row .= '<th style="width: 5em;">' . UOJLocale::get('solved') . '</th>';
$header_row .= '</tr>';
$parsedown = HTML::parsedown();
$purifier = HTML::purifier_inline();
$users = [];
$print_row = function ($user, $now_cnt) use (&$users, $config, $purifier, $parsedown) {
if (!$users) {
if ($now_cnt == 1) {
$rank = 1;
} else {
$rank = DB::selectCount("select count(*) from (select b.username as username, count(*) as accepted from best_ac_submissions a inner join user_info b on a.submitter = b.username group by username) as derived where accepted > {$user['ac_num']}") + 1;
}
} else {
$rank = $now_cnt;
}
$user['rank'] = $rank;
echo '<tr>';
echo '<td>' . $user['rank'] . '</td>';
echo '<td>' . UOJUser::getLink($user['username']) . '</td>';
echo "<td>";
echo $purifier->purify($parsedown->line($user['motto']));
echo "</td>";
echo '<td>' . $user['ac_num'] . '</td>';
echo '</tr>';
$users[] = $user;
};
$from = 'user_info';
$col_names = ['user_info.username as username', 'ac_num', 'motto'];
$cond = '1';
$tail = 'group by user_info.username order by ac_num desc, user_info.username asc';
if (isset($config['group_id'])) {
$group_id = $config['group_id'];
$from = "user_info inner join groups_users on (user_info.username = groups_users.username and groups_users.group_id = {$group_id})";
$config['pagination_cond'] = "group_id = {$group_id}";
}
if (isset($config['top10'])) {
$tail .= ' limit 10';
}
$config['get_row_index'] = '';
echoLongTable($col_names, $from, $cond, $tail, $header_row, $print_row, $config);
}
// ===== uoj.ac =====
function echoJudgmentDetails($raw_details, $styler, $name) {

View File

@ -1,5 +1,6 @@
<?php
return [
'new contest' => 'New Contest',
'current or upcoming contests' => 'Current or upcoming contests',
'ended contests' => 'Ended contests',
'back to the contest' => 'Back to the contest',

View File

@ -1,5 +1,6 @@
<?php
return [
'new contest' => '新建比赛',
'current or upcoming contests' => '正在进行或即将到来的比赛',
'ended contests' => '已结束的比赛',
'back to the contest' => '返回比赛',

View File

@ -52,4 +52,7 @@ return [
'hacks to me' => 'Hacks to me',
'difficulty' => 'Difficulty',
'show difficulty' => 'Show difficulty',
'tags' => 'Tags',
'historical score' => 'Historical Score',
'uploader' => 'Uploader',
];

View File

@ -52,4 +52,7 @@ return [
'hacks to me' => '我的被Hack记录',
'difficulty' => '难度',
'show difficulty' => '显示难度',
'tags' => '标签',
'historical score' => '历史分数',
'uploader' => '上传者',
];

View File

@ -436,7 +436,11 @@ class HTML {
$def->addElement('footer', 'Block', 'Flow', 'Common');
$extra_allowed_html = [
'span' => ['data-realname' => 'Text', 'data-uoj-username' => 'Number'],
'span' => [
'class' => 'Enum#uoj-username',
'data-realname' => 'Text',
'data-color' => 'Color',
],
'img' => ['width' => 'Text'],
];
@ -463,7 +467,11 @@ class HTML {
'small' => [],
'del' => [],
'br' => [],
'span' => ['data-realname' => 'Text', 'data-uoj-username' => 'Number'],
'span' => [
'class' => 'Enum#uoj-username',
'data-realname' => 'Text',
'data-color' => 'Color',
],
];
$allowed_elements = [];
@ -490,8 +498,8 @@ class HTML {
return new HTMLPurifier($config);
}
public static function parsedown() {
return new UOJMarkdown([
public static function parsedown($config = []) {
return new UOJMarkdown($config + [
'math' => [
'enabled' => true,
'matchSingleDollar' => true

View File

@ -258,7 +258,7 @@ class UOJForm {
'options' => [],
'default_value' => '',
'label' => '',
'label_class' => 'form-check-label',
'label_class' => 'form-label',
'help' => '',
'help_class' => 'form-text',
'disabled' => false,

View File

@ -1,10 +1,12 @@
<?php
class UOJMarkdown extends ParsedownMath {
public function __construct($options = '') {
if (method_exists(get_parent_class(),"__construct")) {
if (method_exists(get_parent_class(), "__construct")) {
parent::__construct($options);
}
$this->options['username_with_color'] = $options['username_with_color'] ?: false;
// https://gist.github.com/ShNURoK42/b5ce8baa570975db487c
$this->InlineTypes['@'][] = 'UserMention';
$this->inlineMarkerList .= '@';
@ -19,18 +21,18 @@ class UOJMarkdown extends ParsedownMath {
}
// 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;
}
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()) {
@ -52,27 +54,35 @@ class UOJMarkdown extends ParsedownMath {
}
// https://gist.github.com/ShNURoK42/b5ce8baa570975db487c
protected function inlineUserMention($Excerpt) {
if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) {
if (($user = UOJUser::query($matches[1])) && $user['usergroup'] != 'B') {
return [
'extent' => strlen($matches[0]),
'element' => [
'name' => 'span',
'text' => '@' . $user['username'],
'attributes' => [
'class' => 'uoj-username',
'data-realname' => $user['realname'],
'data-uoj-username' => 1,
],
],
];
}
protected function inlineUserMention($Excerpt) {
if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) {
$mentioned_user = UOJUser::query($matches[1]);
return [
'extent' => strlen($matches[0]),
'markup' => $matches[0],
];
}
}
if ($mentioned_user) {
$color = '#0d6efd';
if ($this->options['username_with_color']) {
$color = UOJUser::getUserColor($mentioned_user);
}
return [
'extent' => strlen($matches[0]),
'element' => [
'name' => 'span',
'text' => '@' . $mentioned_user['username'],
'attributes' => [
'class' => 'uoj-username',
'data-realname' => UOJUser::getRealname($mentioned_user),
'data-color' => $color,
],
],
];
}
return [
'extent' => strlen($matches[0]),
'markup' => $matches[0],
];
}
}
}

View File

@ -22,6 +22,7 @@ class UOJProblem {
2300,
2400,
2500,
2600,
2700,
2900,
3100,
@ -43,13 +44,254 @@ class UOJProblem {
2300 => '#ff8000',
2400 => '#ff8000',
2500 => '#ff8000',
2600 => '#ff0000',
2700 => '#ff0000',
2900 => '#ff0000',
3100 => '#ff0000',
3100 => '#aa0000',
3300 => '#aa0000',
3500 => '#aa0000',
];
public static array $categories = [
'算法基础' => [
'暴力',
'枚举',
'模拟',
'递归与分治',
'贪心',
'排序',
'前缀和与差分',
'二分',
'倍增',
'构造',
'打表',
],
'搜索' => [
'深度优先搜索',
'广度优先搜索',
'双向搜索',
'启发式搜索',
'A*',
'IDA*',
'迭代加深',
'回溯法',
'Dancing Links',
],
'动态规划' => [
'记忆化搜索',
'线性 DP',
'背包 DP',
'区间 DP',
'树形 DP',
'状压 DP',
'数位 DP',
'DAG 上 DP',
'插头 DP',
'概率 DP',
'单调队列优化 DP',
'斜率优化 DP',
'四边形不等式优化 DP',
],
'计算几何' => [
'Pick 定理',
'三角剖分',
'凸包',
'扫描线',
'旋转卡壳',
'半平面交',
'平面最近点对',
'随机增量法',
'反演变换',
],
'数学' => [
'位运算',
'快速幂',
'高精度',
'生成函数',
'指数生成函数',
'向量',
'矩阵',
'高斯消元',
'线性基',
'线性规划',
'容斥',
'组合计数',
'离散对数',
'单纯形算法',
'概率',
'置换群',
'斐波那契数列',
'牛顿迭代法',
'数值积分',
'分块打表',
],
'数论' => [
'最大公约数',
'分解质因数',
'欧拉函数',
'筛法',
'欧拉定理',
'费马小定理',
'类欧几里得算法',
'翡蜀定理',
'乘法逆元',
'线性同余方程',
'Meissel-Lehmer 算法',
'二次剩余',
'BSGS',
'原根',
'卢卡斯定理',
'莫比乌斯反演',
'拉格朗日反演',
'杜教筛',
'Powerful Number 筛',
'Min_25 筛',
'洲阁筛',
'连分数',
'Stern-Brocot 数与 Farey 序列',
'Pell 方程',
],
'字符串' => [
'字符串哈希',
'字典树',
'KMP',
'Boyer-Moore',
'Z 函数(扩展 KMP',
'AC 自动机',
'后缀数组',
'后缀自动机',
'后缀平衡树',
'广义后缀自动机',
'Manacher',
'回文树',
'序列自动机',
'最小表示法',
'Lyndon 分解',
],
'图论' => [
'拓扑排序',
'最短路',
'K 短路',
'同余最短路',
'虚树',
'树分治',
'动态树分治',
'树哈希',
'树上启发式合并',
'AHU 算法',
'矩阵树定理',
'最小生成树',
'最小树形图',
'最小直径生成树',
'斯坦纳树',
'拆点',
'差分约束',
'强连通分量',
'双连通分量',
'割点与桥',
'圆方树',
'2-SAT',
'欧拉图',
'哈密顿图',
'最小环',
'平面图',
'网络流',
'最大流',
'最小割',
'费用流',
'上下界网络流',
'Stoer-Wagner 算法',
'二分图',
'二分图最大匹配',
'二分图最大权匹配',
'一般图最大匹配',
'一般图最大权匹配',
'Prufer 序列',
'LGV 引理',
'弦图',
],
'组合数学' => [
'排列组合',
'卡特兰数',
'斯特林数',
'贝尔数',
'伯努利数',
'康托展开',
'容斥原理',
'抽屉原理',
'欧拉数',
],
'数据结构' => [
'栈',
'队列',
'链表',
'哈希表',
'并查集',
'二叉堆',
'配对堆',
'树状数组',
'线段树',
'平衡树',
'左偏树',
'块状数组',
'块状链表',
'树分块',
'Sqrt Tree',
'可持久化数据结构',
'单调栈',
'单调队列',
'ST 表',
'树套树',
'李超线段树',
'区间最值操作与区间历史最值',
'划分树',
'跳表',
'K-D Tree',
'珂朵莉树',
'动态树',
'析合树',
],
'多项式' => [
'拉格朗日插值',
'快速傅里叶变换',
'快速数论变换',
'快速沃尔什变换',
'多项式求逆',
'多项式开方',
'多项式除法与取模',
'多项式对数函数与指数函数',
'多项式牛顿迭代',
'多项式多点求值与快速插值',
'多项式三角函数',
'多项式反三角函数',
'常系数齐次线性递推',
],
'博弈论' => [
'不平等博弈',
'SG 函数',
'Nim 游戏',
'Anti-Nim',
'纳什均衡',
],
'杂项' => [
'构造',
'离散化',
'CDQ 分治',
'整体二分',
'分块',
'莫队',
'分数规划',
'随机化',
'模拟退火',
'爬山法',
'悬线法',
'编译原理',
'复杂度分析',
'语义分析',
'底层优化',
],
];
public static function query($id) {
if (!isset($id) || !validateUInt($id)) {
return null;

View File

@ -22,7 +22,7 @@ class UOJRanklist {
}
$last_user = null;
$parsedown = HTML::parsedown();
$parsedown = HTML::parsedown(['username_with_color' => true]);
$purifier = HTML::purifier_inline();
$print_row = function ($user, $now_cnt) use (&$last_user, &$conds, &$parsedown, &$purifier) {
if ($last_user === null) {
@ -138,7 +138,7 @@ class UOJRanklist {
$header_row .= '</tr>';
$last_user = null;
$parsedown = HTML::parsedown();
$parsedown = HTML::parsedown(['username_with_color' => true]);
$purifier = HTML::purifier_inline();
$print_row = function ($user, $now_cnt) use (&$last_user, &$conds, &$parsedown, &$purifier) {
if ($last_user === null) {

View File

@ -210,6 +210,43 @@ class UOJUser {
}
}
public static function getRealname($user) {
$realname = $user['realname'];
if ($user['usertype'] == 'teacher') {
$realname .= '老师';
}
return $realname;
}
public static function getUserColor($user) {
$extra = UOJUser::getExtra($user);
return UOJUser::getUserColor2($user['usergroup'], $extra['username_color']);
}
public static function getUserColor2($usergroup, $custom_color = null) {
if ($usergroup == 'B') {
return '#996600';
}
if ($usergroup == 'T') {
return '#707070';
}
if ($usergroup == 'S') {
return $custom_color ?: '#9d3dcf';
}
// 前管理员设置颜色为紫色的,颜色改为蓝色
if ($custom_color == '#9d3dcf') {
return '#0d6efd';
}
return $custom_color ?: '#0d6efd';
}
public static function getLink($user) {
if (is_string($user)) {
$info = UOJUser::query($user);
@ -221,14 +258,18 @@ class UOJUser {
}
}
if ($user['usergroup'] == 'B') {
return HTML::tag('a', ['class' => 'text-danger fw-bold', 'href' => "/user/{$user['username']}"], $user['username']);
}
$realname = UOJUser::getRealname($user);
// 未登录不可查看真实姓名
$realname = Auth::check() ? $user['realname'] : '';
if (!Auth::check()) {
$realname = '';
}
return HTML::tag('span', ['class' => 'uoj-username', 'data-realname' => trim(HTML::escape($realname))], $user['username']);
return HTML::tag('span', [
'class' => 'uoj-username',
'data-color' => UOJUser::getUserColor($user),
'data-realname' => trim(HTML::escape($realname)),
], $user['username']);
}
public static function getUpdatedExtraVisitHistory($history, $cur) {
@ -290,6 +331,7 @@ class UOJUser {
'show_email' => 'all',
'show_qq' => 'all',
'avatar_source' => 'gravatar',
'username_color' => isSuperUser($user) ? '#9d3dcf' : '#0d6efd',
]);
return $extra;
}
@ -340,6 +382,7 @@ class UOJUser {
$extra = UOJUser::getExtra($user);
$cur = [
'addr' => $info['remote_addr'],
'forwarded_addr' => $info['http_x_forwarded_for'],
'ua' => substr($info['http_user_agent'], 0, UOJUser::MAX_UA_LEN),
'last' => UOJTime::$time_now_str
];

View File

@ -92,9 +92,9 @@ Route::group(
Route::any('/click-zan', '/click_zan.php');
// Apps
Route::any('/image_hosting', '/image_hosting/index.php');
Route::get('/image_hosting/{image_name}.png', '/image_hosting/get_image.php');
Route::any('/html2markdown', '/html2markdown.php');
Route::any('/image_hosting', '/app/image_hosting/index.php');
Route::get('/image_hosting/{image_name}.png', '/app/image_hosting/get_image.php');
Route::any('/html2markdown', '/app/html2markdown.php');
}
);

View File

@ -19,6 +19,8 @@ class Parsedown
const version = '1.7.4';
protected $options = [];
# ~
function text($text)

View File

@ -1,7 +1,7 @@
<?php
$reviews = [];
$parsedown = HTML::parsedown();
$parsedown = HTML::parsedown(['username_with_color' => true]);
$purifier = HTML::purifier_inline();
foreach ($contest_data['people'] as $person) {

View File

@ -35,7 +35,7 @@
function(row) {
var col_tr = '<tr>';
col_tr += '<td>' + row[3] + '</td>';
col_tr += '<td>' + getUserLink(row[2][0], row[2][1]) + '</td>';
col_tr += '<td>' + getUserLink(row[2][0], row[2][1], row[2][3]) + '</td>';
col_tr += '<td>' + '<div><span class="uoj-score" data-max="' + problems.length * 100 + '" style="color:' + getColOfScore(row[0] / problems.length) + '">' + row[0] + '</span></div>' + '<div>' + getPenaltyTimeStr(row[1]) + '</div></td>';
for (var i = 0; i < problems.length; i++) {
col = score[row[2][0]][i];

View File

@ -4,11 +4,6 @@ if (!isset($ShowPageFooter)) {
}
?>
</div>
<script>
$(document).ready(function() {
[...document.querySelectorAll('[data-bs-toggle="tooltip"]')].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
});
</script>
<?php if ($ShowPageFooter) : ?>
<?php if (UOJNotice::shouldConstantlyCheckNotice()) : ?>
<script type="text/javascript">

View File

@ -102,6 +102,9 @@ if (!isset($ShowPageHeader)) {
<!-- Color converter -->
<?= HTML::js_src('/js/color-converter.min.js') ?>
<!-- Clipboard Polyfill -->
<?= HTML::js_src('/js/clipboard-polyfill.overwrite-globals.es5.min.js') ?>
<!-- uoj -->
<?= HTML::js_src('/js/uoj.js?v=' . UOJConfig::$data['profile']['s2oj-version']) ?>
@ -175,7 +178,7 @@ if (!isset($ShowPageHeader)) {
}
};
</script>
<script id="MathJax-script" src="<?= HTML::url('/lib/MathJax/tex-mml-chtml.js') ?>"></script>
<script id="MathJax-script" src="<?= HTML::url('/js/mathjax3/tex-mml-chtml.js') ?>"></script>
<?php endif ?>
<?php if (isset($REQUIRE_LIB['jquery.form'])) : ?>

View File

@ -1,6 +1,6 @@
<?php
$purifier = HTML::purifier_inline();
$parsedown = HTML::parsedown();
$parsedown = HTML::parsedown(['username_with_color' => true]);
?>
<?php if (Auth::check()) : ?>

View File

@ -21,7 +21,7 @@
</span>
</h3>
<div class="card-text">
<?= HTML::purifier_inline()->purify(HTML::parsedown()->line($user['motto'])) ?>
<?= HTML::purifier_inline()->purify(HTML::parsedown(['username_with_color' => true])->line($user['motto'])) ?>
</div>
</div>
<ul class="list-group list-group-flush">

View File

@ -25,6 +25,11 @@ a {
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.uoj-realname {
font-size: 90%;
font-weight: normal;
}
h1,
.h1 {
/* font-size: 2.5rem; */
@ -164,7 +169,7 @@ h6,
.card-uoj-tle > .card-header:hover,
.card-uoj-tle > div.card-header > div > .uoj-status-text {
color: sandybrown;
color: #f4a460;
}
.card-uoj-wrong > .card-header:hover,

View File

@ -60,14 +60,6 @@ setWebConf(){
# Set webroot path
ln -sf /opt/uoj/web /var/www/uoj
chown -R www-data /var/www/uoj/app/storage
# Set web config file
php7.4 -a <<UOJEOF
\$config = include '/var/www/uoj/app/.default-config.php';
\$config['database']['host']='$_database_host_';
\$config['database']['password']='$_database_password_';
\$config['judger']['socket']['port']='$_judger_socket_port_';
file_put_contents('/var/www/uoj/app/.config.php', "<?php\nreturn ".str_replace('\'_httpHost_\'','UOJContext::requestDomain()',var_export(\$config, true)).";\n");
UOJEOF
# Prepare local sandbox
cd /opt/uoj/judger/uoj_judger
cat >include/uoj_work_path.h <<UOJEOF
@ -84,9 +76,6 @@ initProgress(){
#Set uoj_data path
mkdir -p /var/uoj_data/upload
chown -R www-data:www-data /var/uoj_data
#Replace password placeholders
sed -i -e "s/salt0/$_salt0_/g" -e "s/salt1/$_salt1_/g" -e "s/salt2/$_salt2_/g" -e "s/salt3/$_salt3_/g" -e "s/_judger_socket_password_/$_judger_socket_password_/g" /var/www/uoj/app/.config.php
sed -i -e "s/'protocol' => 'http'/'protocol' => '$_uoj_protocol_'/g" /var/www/uoj/app/.config.php
#Start services
service ntp restart
service apache2 restart

View File

@ -0,0 +1,2 @@
/*! clipboard-polyfill v4.0.0-rc8 | MIT License | github.com/zenorocha/clipboard.js */
"use strict";!function(){var e="text/plain";(function(){(console.warn||console.log).apply(console,arguments)}).bind("[clipboard-polyfill]");var n,t,r,o="undefined"==typeof window?void 0:window,i="undefined"==typeof globalThis?void 0:globalThis,a=null!=(r=null==(n=o)?void 0:n.Promise)?r:null==(t=i)?void 0:t.Promise;var u,l,c,d,f,v="undefined"==typeof navigator?void 0:navigator,s=null==v?void 0:v.clipboard,p=null==(u=null==s?void 0:s.read)?void 0:u.bind(s),b=null==(l=null==s?void 0:s.readText)?void 0:l.bind(s),m=null==(c=null==s?void 0:s.write)?void 0:c.bind(s),y=null==(d=null==s?void 0:s.writeText)?void 0:d.bind(s),w=null==(f=o)?void 0:f.ClipboardItem,h=function(){if(!a)throw new Error("No `Promise` implementation available for `clipboard-polyfill`. Consider using: https://github.com/lgarron/clipboard-polyfill#flat-file-version-with-promise-included");return a}(),g=o;function x(){return"undefined"==typeof ClipboardEvent&&void 0!==(null==g?void 0:g.clipboardData)&&void 0!==(null==g?void 0:g.clipboardData.setData)}function E(n,t,r){for(var o in n.success=!0,t){var i=t[o],a=r.clipboardData;a.setData(o,i),o===e&&a.getData(o)!==i&&(n.success=!1)}r.preventDefault()}function C(e){var n={success:!1},t=E.bind(this,n,e);document.addEventListener("copy",t);try{document.execCommand("copy")}finally{document.removeEventListener("copy",t)}return n.success}function T(e,n){D(e);var t=C(n);return S(),t}function D(e){var n=document.getSelection();if(n){var t=document.createRange();t.selectNodeContents(e),n.removeAllRanges(),n.addRange(t)}}function S(){var e=document.getSelection();e&&e.removeAllRanges()}function A(n){var t,r=e in n;if(x()){if(!r)throw new Error("No `text/plain` value was specified.");if(t=n[e],g.clipboardData.setData("Text",t))return!0;throw new Error("Copying failed, possibly because the user rejected it.")}return!!C(n)||(navigator.userAgent.indexOf("Edge")>-1||(!!T(document.body,n)||(!!function(e){var n=document.createElement("div");n.setAttribute("style","-webkit-user-select: text !important"),n.textContent="temporary element",document.body.appendChild(n);var t=T(n,e);return document.body.removeChild(n),t}(n)||!!function(e){var n=document.createElement("div");n.setAttribute("style","-webkit-user-select: text !important");var t=n;n.attachShadow&&(t=n.attachShadow({mode:"open"}));var r=document.createElement("span");r.innerText=e,t.appendChild(r),document.body.appendChild(n),D(r);var o=document.execCommand("copy");return S(),document.body.removeChild(n),o}(n[e]))))}function R(e,n){var t=[];for(var r in e){var o=e[r];t.push(n(o))}return h.all(t).then((function(n){for(var t={},r=0;r<e.length;r++)t[e[r]]=n[r];return t}))}var k=h.resolve(),L=function(){return h.resolve(!0)},N=h.resolve(!1);function O(e){return new h((function(n,t){try{n(e())}catch(e){t(e)}}))}function P(n){if(!A(function(n){var t={};return t[e]=n,t}(n)))throw new Error("writeText() failed")}function j(){return O((function(){if(b)return b();if(x()){var e=function(){var e=g.clipboardData.getData("Text");if(""===e)throw new Error("Empty clipboard or could not read plain text from clipboard");return e}();return h.resolve(e)}throw new Error("Read is not supported in your browser.")}))}function I(e,n){for(var t in e){if(-1!==e[t].types.indexOf(n))return!0}return!1}var B=function(e,n){var t,r=Object.keys(e),o={};for(var i in e){var a=e[i];o[i]="string"==typeof a?F(i,a):a}return{types:r,presentationStyle:null!=(t=null==n?void 0:n.presentationStyle)?t:"unspecified",getType:function(e){return h.resolve(o[e])}}};function F(e,n){return new Blob([n],{type:e})}function q(e){return R(e.types,(function(n){return e.getType(n)})).then((function(n){var t={};return e.presentationStyle&&(t.presentationStyle=e.presentationStyle),new w(n,t)}))}function z(n){var t={};return t[e]=F(n,e),new B(t)}function G(e,n){return e.getType(n).then((function(e){return n=e,new h((function(e,t){var r=new FileReader;r.addEventListener("load",(function(){var n=r.result;"string"==typeof n?e(n):t("could not convert blob to string")})),r.readAsText(n)}));var n}))}navigator.clipboard||(navigator.clipboard={}),navigator.clipboard.read=function(){return O((function(){return p?p():j().then((function(e){return[z(e)]}))}))},navigator.clipboard.readText=j,navigator.clipboard.write=function(n){return O((function(){if(m&&w){var t=m;return h.all(n.map(q)).then((function(r){return t(r).then(L).catch((function(t){if(!I(n,e)&&!I(n,"text/html"))throw t;return N}))}))}return N})).then((function(t){if(t)return k;I(n,e);return function(e){return R(e.types,(function(n){return G(e,n)}))}(n[0]).then((function(e){if(!A(e))throw new Error("write() failed")}))}))},navigator.clipboard.writeText=function(e){return O((function(){return y?y(e).catch(P):h.resolve(P(e))}))},window.ClipboardItem=B}();

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More