Explorar el Código

Initial commit 20260810

ryp hace 1 semana
commit
9b2096aad7
Se han modificado 100 ficheros con 13264 adiciones y 0 borrados
  1. 9 0
      .gitignore
  2. 28 0
      .htaccess
  3. 18 0
      .php-cs-fixer.php
  4. 33 0
      Dockerfile
  5. 63 0
      app/.default-config.php
  6. 1 0
      app/.htaccess
  7. 89 0
      app/cli.php
  8. BIN
      app/controllers/.cloud.php.swo
  9. BIN
      app/controllers/.cloud.php.swp
  10. BIN
      app/controllers/.groups_manage.php.swp
  11. BIN
      app/controllers/.problem_set.php.swo
  12. BIN
      app/controllers/.submission.php.swp
  13. BIN
      app/controllers/.user_info.php.swp
  14. BIN
      app/controllers/.user_msg.php.swn
  15. BIN
      app/controllers/.user_msg.php.swo
  16. 55 0
      app/controllers/add_contest.php
  17. 43 0
      app/controllers/announcements.php
  18. 7 0
      app/controllers/blog_show.php
  19. 32 0
      app/controllers/blogs.php
  20. 160 0
      app/controllers/certs_manage.php
  21. 188 0
      app/controllers/change_user_info.php
  22. 68 0
      app/controllers/click_zan.php
  23. 77 0
      app/controllers/cloud.php
  24. 556 0
      app/controllers/contest_inside.php
  25. 285 0
      app/controllers/contest_manage.php
  26. 111 0
      app/controllers/contest_members.php
  27. 37 0
      app/controllers/contest_registration.php
  28. 103 0
      app/controllers/contests.php
  29. 49 0
      app/controllers/datum.php
  30. 83 0
      app/controllers/datums.php
  31. 50 0
      app/controllers/download.php
  32. 70 0
      app/controllers/export_contest_standings_table.php
  33. 210 0
      app/controllers/faq.php
  34. 59 0
      app/controllers/forgot_pw.php
  35. 90 0
      app/controllers/group_info.php
  36. 164 0
      app/controllers/group_manage.php
  37. 105 0
      app/controllers/groups_manage.php
  38. 86 0
      app/controllers/hack.php
  39. 101 0
      app/controllers/hack_list.php
  40. 279 0
      app/controllers/index.php
  41. 43 0
      app/controllers/judge/download.php
  42. 212 0
      app/controllers/judge/submit.php
  43. 21 0
      app/controllers/judge/sync_judge_client.php
  44. 120 0
      app/controllers/login.php
  45. 12 0
      app/controllers/logout.php
  46. 132 0
      app/controllers/map_visualizer.php
  47. 37 0
      app/controllers/paste_post.php
  48. 12 0
      app/controllers/paste_view.php
  49. 276 0
      app/controllers/problem.php
  50. 821 0
      app/controllers/problem_data_manage.php
  51. 821 0
      app/controllers/problem_data_manage_back.php
  52. 59 0
      app/controllers/problem_managers_manage.php
  53. 372 0
      app/controllers/problem_set.php
  54. 58 0
      app/controllers/problem_statement_manage.php
  55. 169 0
      app/controllers/problem_statistics.php
  56. 10 0
      app/controllers/ranklist.php
  57. 186 0
      app/controllers/register.php
  58. 103 0
      app/controllers/reset_pw.php
  59. 55 0
      app/controllers/rp.php
  60. 62 0
      app/controllers/rps.php
  61. BIN
      app/controllers/subdomain/blog/.archive.php.swp
  62. 15 0
      app/controllers/subdomain/blog/aboutme.php
  63. 164 0
      app/controllers/subdomain/blog/archive.php
  64. 196 0
      app/controllers/subdomain/blog/blog.php
  65. 25 0
      app/controllers/subdomain/blog/blog_delete.php
  66. 88 0
      app/controllers/subdomain/blog/blog_write.php
  67. 31 0
      app/controllers/subdomain/blog/index.php
  68. 51 0
      app/controllers/subdomain/blog/route.php
  69. 15 0
      app/controllers/subdomain/blog/slide.php
  70. 85 0
      app/controllers/subdomain/blog/slide_write.php
  71. 238 0
      app/controllers/submission.php
  72. 35 0
      app/controllers/submission_status_details.php
  73. 83 0
      app/controllers/submissions_list.php
  74. 619 0
      app/controllers/super_manage.php
  75. 271 0
      app/controllers/user_info.php
  76. 306 0
      app/controllers/user_msg.php
  77. 26 0
      app/controllers/user_system_msg.php
  78. BIN
      app/libs/.uoj-contest-lib.php.swp
  79. BIN
      app/libs/.uoj-html-lib.php.swo
  80. BIN
      app/libs/.uoj-query-lib.php.swo
  81. 243 0
      app/libs/uoj-contest-lib.php
  82. 368 0
      app/libs/uoj-data-lib.php
  83. 809 0
      app/libs/uoj-form-lib.php
  84. 1330 0
      app/libs/uoj-html-lib.php
  85. 186 0
      app/libs/uoj-judger-lib.php
  86. 33 0
      app/libs/uoj-lib.php
  87. 354 0
      app/libs/uoj-query-lib.php
  88. 37 0
      app/libs/uoj-rand-lib.php
  89. 33 0
      app/libs/uoj-security-lib.php
  90. 187 0
      app/libs/uoj-utility-lib.php
  91. 49 0
      app/libs/uoj-validate-lib.php
  92. 69 0
      app/locale/basic/en.php
  93. 69 0
      app/locale/basic/zh-cn.php
  94. 29 0
      app/locale/contests/en.php
  95. 29 0
      app/locale/contests/zh-cn.php
  96. 47 0
      app/locale/problems/en.php
  97. 47 0
      app/locale/problems/zh-cn.php
  98. 83 0
      app/models/Auth.php
  99. 46 0
      app/models/Cookie.php
  100. 78 0
      app/models/DB.php

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+.idea
+.php_cs.cache
+app/.config.php
+app/storage/tmp/*
+app/storage/submission/*
+app/storage/paste/*
+!app/storage/tmp/.gitkeep
+!app/storage/submission/.gitkeep
+!app/storage/paste/.gitkeep

+ 28 - 0
.htaccess

@@ -0,0 +1,28 @@
+Options -Indexes
+
+php_value session.save_path /var/lib/php/uoj_sessions
+php_value session.gc_maxlifetime 172800
+php_value session.cookie_lifetime 31536000
+
+php_value post_max_size 1000M
+php_value upload_max_filesize 1000M
+
+php_value session.gc_probability 1
+php_value session.gc_divisor 1000
+
+php_value date.timezone 'Asia/Shanghai'
+
+DirectorySlash Off
+
+DirectoryIndex
+
+RewriteEngine On
+
+RewriteCond %{QUERY_STRING} ^$
+RewriteRule ^(.*)/$ /$1 [L,R=301]
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^ index.php [L]
+
+Header set Access-Control-Allow-Origin "*"
+

+ 18 - 0
.php-cs-fixer.php

@@ -0,0 +1,18 @@
+<?php
+
+$finder = PhpCsFixer\Finder::create()
+	->exclude('vendor')
+	->in(__DIR__)
+;
+
+$config = new PhpCsFixer\Config();
+return $config
+	->setIndent("\t")
+	->setRules([
+		'braces'=>[
+			'position_after_functions_and_oop_constructs'=>'same'
+		],
+		'elseif' => true
+	])
+	->setFinder($finder)
+	;

+ 33 - 0
Dockerfile

@@ -0,0 +1,33 @@
+FROM ubuntu:20.04
+MAINTAINER Baoshuo <i@baoshuo.ren>
+LABEL org.opencontainers.image.source=https://github.com/UniversalOJ/UOJ-System
+LABEL org.opencontainers.image.description="UOJ Web"
+LABEL org.opencontainers.image.licenses=MIT
+
+ARG CLONE_ADDFLAG
+
+ENV DEBIAN_FRONTEND=noninteractive
+COPY v8js.zip /tmp/pear/download/
+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 && export OJ_PUSHD=`pwd` && cd /tmp/pear/download &&\
+unzip v8js.zip && cd v8js &&\
+phpize && ./configure --with-php-config=/usr/bin/php-config --with-v8js=/opt/libv8-7.5 && make install && cd $OJ_PUSHD && unset OJ_PUSHD
+
+ADD . /opt/uoj
+WORKDIR /opt/uoj
+
+# Install environment and set startup script
+RUN sh web/install.sh -p && echo "\
+#!/bin/sh\n\
+if [ ! -f \"/var/uoj_data/.UOJSetupDone\" ]; then\n\
+  cd /opt/uoj/web && sh install.sh -i\n\
+fi\n\
+service ntp start\n\
+service apache2 start\n\
+exec bash\n" >/opt/up && chmod +x /opt/up
+
+ENV LANG=C.UTF-8 TZ=Asia/Shanghai
+EXPOSE 80
+CMD /opt/up

+ 63 - 0
app/.default-config.php

@@ -0,0 +1,63 @@
+<?php
+return [
+	'profile' => [
+		'oj-name' => 'Universal Online Judge',
+		'oj-name-short' => 'UOJ',
+		'administrator' => 'root',
+		'admin-email' => 'admin@local_uoj.ac',
+		'QQ-group' => '',
+		'ICP-license' => ''
+	],
+	'database' => [
+		'database' => 'app_uoj233',
+		'username' => 'root',
+		'password' => 'root',
+		'host' => 'uoj-db'
+	],
+	'web' => [
+		'domain' => null,
+		'main' => [
+			'protocol' => 'http',
+			'host' => '_httpHost_',
+			'port' => 80
+		],
+		'blog' => [
+			'protocol' => 'http',
+			'host' => '_httpHost_',
+			'port' => 80
+		]
+	],
+	'security' => [
+		'user' => [
+			'client_salt' => 'salt0'
+		],
+		'cookie' => [
+			'checksum_salt' => ['salt1', 'salt2', 'salt3']
+		],
+	],
+	'mail' => [
+		'noreply' => [
+			'username' => 'noreply@local_uoj.ac',
+			'password' => '_mail_noreply_password_',
+			'host' => 'smtp.local_uoj.ac',
+			'secure' => 'tls',
+			'port' => 587
+		]
+	],
+	'judger' => [
+		'socket' => [
+			'port' => '233',
+			'password' => '_judger_socket_password_'
+		]
+	],
+	'switch' => [
+		// 请在 page-header.php 中修改统计代码后再启用
+		'web-analytics' => false,
+		'blog-domain-mode' => 3
+	],
+	'tools' => [
+		// 请仅在https下启用以下功能.
+		// 非https下, chrome无法进行复制.
+		'map-copy-enabled' => false,
+	]
+];

+ 1 - 0
app/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 89 - 0
app/cli.php

@@ -0,0 +1,89 @@
+<?php
+
+$_SERVER['DOCUMENT_ROOT'] = dirname(__DIR__);
+
+require $_SERVER['DOCUMENT_ROOT'] . '/app/libs/uoj-lib.php';
+
+// TODO: more beautiful argv parser
+
+$handlers = [
+	'upgrade:up' => function ($name) {
+		if (func_num_args() != 1) {
+			die("php cli.php upgrade:up <name>\n");
+		}
+		Upgrader::transaction(function() use ($name) {
+			Upgrader::up($name);
+		});
+		die("finished!\n");
+	},
+	'upgrade:down' => function ($name) {
+		if (func_num_args() != 1) {
+			die("php cli.php upgrade:down <name>\n");
+		}
+		Upgrader::transaction(function() use ($name) {
+			Upgrader::down($name);
+		});
+		die("finished!\n");
+	},
+	'upgrade:refresh' => function ($name) {
+		if (func_num_args() != 1) {
+			die("php cli.php upgrade:refresh <name>\n");
+		}
+		Upgrader::transaction(function() use ($name) {
+			Upgrader::refresh($name);
+		});
+		die("finished!\n");
+	},
+	'upgrade:remove' => function ($name) {
+		if (func_num_args() != 1) {
+			die("php cli.php upgrade:remove <name>\n");
+		}
+		Upgrader::transaction(function() use ($name) {
+			Upgrader::remove($name);
+		});
+		die("finished!\n");
+	},
+	'upgrade:latest' => function () {
+		if (func_num_args() != 0) {
+			die("php cli.php upgrade:latest\n");
+		}
+		Upgrader::transaction(function() {
+			Upgrader::upgradeToLatest();
+		});
+		die("finished!\n");
+	},
+	'upgrade:remove-all' => function () {
+		if (func_num_args() != 0) {
+			die("php cli.php upgrade:remove-all\n");
+		}
+		Upgrader::transaction(function() {
+			Upgrader::removeAll();
+		});
+		die("finished!\n");
+	},
+	'help' => 'showHelp'
+];
+
+function showHelp() {
+	global $handlers;
+	echo "UOJ Command-Line Interface\n";
+	echo "php cli.php <task-name> params1 params2 ...\n";
+	echo "\n";
+	echo "The following tasks are available:\n";
+	foreach ($handlers as $cmd => $handler) {
+		echo "\t$cmd\n";
+	}
+}
+
+if (count($argv) <= 1) {
+	showHelp();
+	die();
+}
+
+if (!isset($handlers[$argv[1]])) {
+	echo "Invalid parameters.\n";
+	showHelp();
+	die();
+}
+
+call_user_func_array($handlers[$argv[1]], array_slice($argv, 2));

BIN
app/controllers/.cloud.php.swo


BIN
app/controllers/.cloud.php.swp


BIN
app/controllers/.groups_manage.php.swp


BIN
app/controllers/.problem_set.php.swo


BIN
app/controllers/.submission.php.swp


BIN
app/controllers/.user_info.php.swp


BIN
app/controllers/.user_msg.php.swn


BIN
app/controllers/.user_msg.php.swo


+ 55 - 0
app/controllers/add_contest.php

@@ -0,0 +1,55 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!isSuperUser($myUser)) {
+		become403Page();
+	}
+	$time_form = new UOJForm('time');
+	$time_form->addInput(
+		'name', 'text', '比赛标题', 'New Contest',
+		function($str) {
+			return '';
+		},
+		null
+	);
+	$time_form->addInput(
+		'start_time', 'text', '开始时间', date("Y-m-d H:i:s"),
+		function($str, &$vdata) {
+			try {
+				$vdata['start_time'] = new DateTime($str);
+			} catch (Exception $e) {
+				return '无效时间格式';
+			}
+			return '';
+		},
+		null
+	);
+	$time_form->addInput(
+		'last_min', 'text', '时长(单位:分钟)', 180,
+		function($str) {
+			return !validateUInt($str) ? '必须为一个整数' : '';
+		},
+		null
+	);
+	$time_form->handle = function(&$vdata) {
+		$start_time_str = $vdata['start_time']->format('Y-m-d H:i:s');
+				
+		$purifier = HTML::pruifier();
+		
+		$esc_name = $_POST['name'];
+		$esc_name = $purifier->purify($esc_name);
+		$esc_name = DB::escape($esc_name);
+		
+		DB::query("insert into contests (name, start_time, last_min, status) values ('$esc_name', '$start_time_str', ${_POST['last_min']}, 'unfinished')");
+	};
+	$time_form->succ_href="/contests";
+	$time_form->runAtServer();
+?>
+<?php echoUOJPageHeader('添加比赛') ?>
+<h1 class="page-header">添加比赛</h1>
+<div class="tab-pane active" id="tab-time">
+<?php
+	$time_form->printHTML();
+?>
+</div>
+<?php echoUOJPageFooter() ?>

+ 43 - 0
app/controllers/announcements.php

@@ -0,0 +1,43 @@
+<?php
+	requirePHPLib('form');
+	
+	function echoBlogCell($blog) {
+		$level = $blog['level'];
+		
+		switch ($level) {
+			case 0:
+				$level_str = '';
+				break;
+			case 1:
+				$level_str = '<span style="color:red">[三级置顶]</span> ';
+				break;
+			case 2:
+				$level_str = '<span style="color:red">[二级置顶]</span> ';
+				break;
+			case 3:
+				$level_str = '<span style="color:red">[一级置顶]</span> ';
+				break;
+		}
+		
+		echo '<tr>';
+		echo '<td>' . $level_str . getBlogLink($blog['id']) . '</td>';
+		echo '<td>' . getUserLink($blog['poster']) . '</td>';
+		echo '<td>' . $blog['post_time'] . '</td>';
+		echo '</tr>';
+	}
+	$header = <<<EOD
+	<tr>
+		<th width="60%">标题</th>
+		<th width="20%">发表者</th>
+		<th width="20%">发表日期</th>
+	</tr>
+EOD;
+	$config = [
+		'table_classes' => ['table', 'table-hover'],
+		'page_len' => 100
+	];
+?>
+<?php echoUOJPageHeader(UOJLocale::get('announcements')) ?>
+<h3>公告</h3>
+<?php echoLongTable(array('blogs.id', 'poster', 'title', 'post_time', 'zan', 'level'), 'important_blogs, blogs', 'is_hidden = 0 and important_blogs.blog_id = blogs.id', 'order by level desc, important_blogs.blog_id desc', $header, 'echoBlogCell', $config); ?>
+<?php echoUOJPageFooter() ?>

+ 7 - 0
app/controllers/blog_show.php

@@ -0,0 +1,7 @@
+<?php
+	if (!validateUInt($_GET['id']) || !($blog = queryBlog($_GET['id']))) {
+		become404Page();
+	}
+	
+	redirectTo(HTML::blog_url($blog['poster'], '/post/'.$_GET['id']));
+?>

+ 32 - 0
app/controllers/blogs.php

@@ -0,0 +1,32 @@
+<?php
+	requirePHPLib('form');
+	
+	function echoBlogCell($blog) {
+		echo '<tr>';
+		echo '<td>' . getBlogLink($blog['id']) . '</td>';
+		echo '<td>' . getUserLink($blog['poster']) . '</td>';
+		echo '<td>' . $blog['post_time'] . '</td>';
+		echo '</tr>';
+	}
+	$header = <<<EOD
+	<tr>
+		<th width="55%">标题</th>
+		<th width="25%">发表者</th>
+		<th width="20%">发表日期</th>
+	</tr>
+EOD;
+	$config = array();
+	$config['table_classes'] = array('table', 'table-hover');
+?>
+<?php echoUOJPageHeader(UOJLocale::get('blogs')) ?>
+<?php if (Auth::check()): ?>
+<div class="float-right">
+	<div class="btn-group">
+		<a href="<?= HTML::blog_url(Auth::id(), '/') ?>" class="btn btn-secondary btn-sm">我的博客首页</a>
+		<a href="<?= HTML::blog_url(Auth::id(), '/post/new/write')?>" class="btn btn-primary btn-sm"><span class="glyphicon glyphicon-edit"></span> 写新博客</a>
+	</div>
+</div>
+<?php endif ?>
+<h3>博客总览</h3>
+<?php echoLongTable(array('id', 'poster', 'title', 'post_time', 'zan'), 'blogs', 'is_hidden = 0', 'order by post_time desc, id desc', $header, 'echoBlogCell', $config); ?>
+<?php echoUOJPageFooter() ?>

+ 160 - 0
app/controllers/certs_manage.php

@@ -0,0 +1,160 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	
+	if ($myUser == null) {
+		become403Page();
+	}
+	
+	$cur_tab = isset($_GET['tab']) ? $_GET['tab'] : 'my';
+	
+	$tabs_info = array(
+		'my' => array (
+			'name' => '我的勋章',
+			'url' => '/certs-manage/my'
+		),
+		'aboutme' => array (
+			'name' => '自定义页面',
+			'url' => '/certs-manage/aboutme'
+		),
+		'songs' => array (
+			'name' => '我要推歌',
+			'url' => '/certs-manage/songs'
+		),
+		'luogu' => array (
+			'name' => 'Luogu 账号管理',
+			'url' => '/certs-manage/luogu'
+		)
+
+	);
+
+	if (!isset($tabs_info[$cur_tab])) {
+		become404Page();
+	}
+
+	if ($_POST['certs'] != '') {
+		$certs = DB::escape($_POST['certs']);
+		$username = $myUser['username'];
+		if (!in_array($certs, explode(',', $myUser['certslist'])))
+			becomeMsgPage('<div>你好像没有这个勋章</div>');
+		DB::update("update user_info set certs = \"$certs\" where username = \"$username\"");
+		becomeMsgPage("<div><h4>成功切换!</h4></div>");
+	}
+
+	$aboutme_form = new UOJForm('aboutme_form');
+	$aboutme_form->addInput('blogid', 'text', '博客 ID', '',
+		function ($x) {
+			if (!validateUInt($x)) return 'ID 不合法';
+			if (!queryBlog($x)) return '博客不存在';
+			return '';
+		}, null);
+
+	$aboutme_form->handle = function () {
+		global $myUser;
+		$id = $_POST['blogid'];
+		DB::update("update user_info set aboutme_blogid = $id where username = '{$myUser['username']}'");
+	};
+
+	$aboutme_form->runAtServer();
+
+	$luogu_form = new UOJForm('luogu_form');
+	$luogu_form->addInput('luogu_uid', 'text', 'Luogu UID', '', 
+		function ($x) {
+			if (!validateUInt($x))
+				return '不是整数';
+			return '';
+		}, null);
+	$luogu_form->addInput('luogu_clientid', 'text', '__client_id(在 Cookie 中)', '', 
+		function ($x) {
+			return '';
+		}, null);
+	$luogu_form->handle = function () {
+		global $myUser;
+		$luid = DB::escape($_POST['luogu_uid']);
+		$lcid = DB::escape($_POST['luogu_clientid']);
+		$username = $myUser['username'];
+		DB::update("update user_info set luogu_uid = '$luid', luogu_clientid = '$lcid' where username = '$username'");
+	};
+
+	$luogu_form->runAtServer();
+
+	$song_form = new UOJForm('songs_form');
+	$song_form->addInput('song_name', 'text', '歌名', '', 
+		function ($x) {
+			if (strlen($x) > 200)
+				return '歌名太长了';
+			return '';
+		}, null);
+	$song_form->addInput('writer_name', 'text', '作者', '',
+		function ($x) {
+			if (strlen($x) > 200)
+				return '作者名字太长了';
+			return '';
+		}, null);
+	$song_form->addInput('album_name', 'text', '专辑', '',
+		function ($x) {
+			if (strlen($x) > 200)
+				return '专辑名太长了';
+			return '';
+		}, null);
+	$song_form->addInput('cover_url', 'text', '封面链接', '',
+		function ($x) {
+			if (strlen($x) > 256)
+				return '封面链接过长';
+			return '';
+		}, null);
+	$song_form->addInput('click_url', 'text', '歌曲链接', '',
+		function ($x) {
+			if (strlen($x) > 256)
+				return '歌曲链接过长';
+			return '';
+		}, null);
+	$song_form->handle = function () {
+		global $myUser;
+		$song_name = DB::escape(HTML::escape($_POST['song_name']));
+		$writer_name = DB::escape(HTML::escape($_POST['writer_name']));
+		$album_name = DB::escape(HTML::escape($_POST['album_name']));
+		$cover_url = DB::escape(HTML::escape($_POST['cover_url']));
+		$click_url = DB::escape(HTML::escape($_POST['click_url']));
+		$username = $myUser['username'];
+		DB::insert("insert into songs (name, writer, album, cover, click, user) values " . 
+			"('$song_name', '$writer_name', '$album_name', '$cover_url', '$click_url', '$username')");
+	};
+	$song_form->runAtServer();
+?>
+<?php
+	requireLib('hljs');
+	requireLib('morris');
+?>
+<?php echoUOJPageHeader('勋章管理') ?>
+<div class="row">
+	<div class="col-sm-3">
+		<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills flex-column') ?>
+	</div>
+	
+	<div class="col-sm-9">
+		<?php if ($cur_tab == 'my'): ?>
+			<div>
+				<h4>我的勋章</h4>
+				<?php echoCertsList($myUser) ?>
+			</div>
+		<?php elseif ($cur_tab == 'aboutme'): ?>
+			<div>
+				<h4>设置自定义页面</h4>
+				<?php $aboutme_form->printHTML(); ?>
+			</div>
+		<?php elseif ($cur_tab == 'songs'): ?>
+			<div>
+				<h4>我要推歌!</h4>
+				<?php $song_form->printHTML(); ?>
+			</div>
+		<?php elseif ($cur_tab == 'luogu'): ?>
+			<div>
+				<h4>Luogu 账号管理</h4>
+				<?php $luogu_form->printHTML(); ?>
+			</div>
+
+		<?php endif ?>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 188 - 0
app/controllers/change_user_info.php

@@ -0,0 +1,188 @@
+<?php
+	if (!Auth::check()) {
+		redirectToLogin();
+	}
+	function handlePost() {
+		global $myUser;
+		if (!isset($_POST['old_password'])) {
+			return '无效表单';
+		}
+		$old_password = $_POST['old_password'];
+		if (!validatePassword($old_password) || !checkPassword($myUser, $old_password)) {
+			return "失败:密码错误。";
+		}
+		if ($_POST['ptag']) {
+			$password = $_POST['password'];
+			if (!validatePassword($password)) {
+				return "失败:无效密码。";
+			}
+			$password = getPasswordToStore($password, $myUser['username']);
+			DB::update("update user_info set password = '$password' where username = '{$myUser['username']}'");
+		}
+
+		$email = $_POST['email'];
+		if (!validateEmail($email)) {
+			return "失败:无效电子邮箱。";
+		}
+		$esc_email = DB::escape($email);
+		DB::update("update user_info set email = '$esc_email' where username = '{$myUser['username']}'");
+
+		if ($_POST['Qtag']) {
+			$qq = $_POST['qq'];
+			if ($qq != $myUser['qq'])
+				return "失败:不能更改 QQ。";
+			if (!validateQQ($qq)) {
+				return "失败:无效QQ。";
+			}
+			$esc_qq = DB::escape($qq);
+			DB::update("update user_info set qq = '$esc_qq' where username = '{$myUser['username']}'");
+		} else {
+			DB::update("update user_info set QQ = NULL where username = '{$myUser['username']}'");
+		}
+		if ($_POST['sex'] == "U" || $_POST['sex'] == 'M' || $_POST['sex'] == 'F') {
+			$sex = $_POST['sex'];
+			$esc_sex = DB::escape($sex);
+			DB::update("update user_info set sex = '$esc_sex' where username = '{$myUser['username']}'");
+		}
+		
+		if (validateMotto($_POST['motto'])) {
+			$esc_motto = DB::escape($_POST['motto']);
+			DB::update("update user_info set motto = '$esc_motto' where username = '{$myUser['username']}'");
+		}
+		return "ok";
+	}
+	if (isset($_POST['change'])) {
+		die(handlePost());
+	}
+?>
+<?php
+	$REQUIRE_LIB['dialog'] = '';
+	$REQUIRE_LIB['md5'] = '';
+?>
+<?php echoUOJPageHeader(UOJLocale::get('modify my profile')) ?>
+<h2 class="page-header"><?= UOJLocale::get('modify my profile') ?></h2>
+<form id="form-update" class="form-horizontal">
+	<h4><?= UOJLocale::get('please enter your password for authorization') ?></h4>
+	<div id="div-old_password" class="form-group">
+		<label for="input-old_password" class="col-sm-2 control-label"><?= UOJLocale::get('password') ?></label>
+		<div class="col-sm-3">
+			<input type="password" class="form-control" name="old_password" id="input-old_password" placeholder="<?= UOJLocale::get('enter your password') ?>" maxlength="20" />
+			<span class="help-block" id="help-old_password"></span>
+		</div>
+	</div>
+	<h4><?= UOJLocale::get('please enter your new profile') ?></h4>
+	<div id="div-password" class="form-group">
+		<label for="input-password" class="col-sm-2 control-label"><?= UOJLocale::get('new password') ?></label>
+		<div class="col-sm-3">
+			<input type="password" class="form-control" id="input-password" name="password" placeholder="<?= UOJLocale::get('enter your new password') ?>" maxlength="20" />
+			<input type="password" class="form-control top-buffer-sm" id="input-confirm_password" placeholder="<?= UOJLocale::get('re-enter your new password') ?>" maxlength="20" />
+			<span class="help-block" id="help-password"><?= UOJLocale::get('leave it blank if you do not want to change the password') ?></span>
+		</div>
+	</div>
+	<div id="div-email" class="form-group">
+		<label for="input-email" class="col-sm-2 control-label"><?= UOJLocale::get('email') ?></label>
+		<div class="col-sm-3">
+			<input type="email" class="form-control" name="email" id="input-email" value="<?=$myUser['email']?>" placeholder="<?= UOJLocale::get('enter your email') ?>" maxlength="50" />
+			<span class="help-block" id="help-email"></span>
+		</div>
+	</div>
+	<div id="div-qq" class="form-group">
+		<label for="input-qq" class="col-sm-2 control-label"><?= UOJLocale::get('QQ') ?></label>
+		<div class="col-sm-3">
+			<input type="text" class="form-control" name="qq" id="input-qq" value="<?= $myUser['qq'] != 0 ? $myUser['qq'] : '' ?>" placeholder="<?= UOJLocale::get('enter your QQ') ?>" maxlength="50" />
+			<span class="help-block" id="help-qq"></span>
+		</div>
+	</div>
+	<div id="div-sex" class="form-group">
+		<label for="input-sex" class="col-sm-2 control-label"><?= UOJLocale::get('sex') ?></label>
+		<div class="col-sm-3">
+			<select class="form-control" id="input-sex"  name="sex">
+				<option value="U"<?= Auth::user()['sex'] == 'U' ? ' selected="selected"' : ''?>><?= UOJLocale::get('refuse to answer') ?></option>
+				<option value="M"<?= Auth::user()['sex'] == 'M' ? ' selected="selected"' : ''?>><?= UOJLocale::get('male') ?></option>
+				<option value="F"<?= Auth::user()['sex'] == 'F' ? ' selected="selected"' : ''?>><?= UOJLocale::get('female') ?></option>
+			</select>
+		</div>
+	</div>
+	<div id="div-motto" class="form-group">
+		<label for="input-motto" class="col-sm-2 control-label"><?= UOJLocale::get('motto') ?></label>
+		<div class="col-sm-3">
+			<textarea class="form-control" id="input-motto"  name="motto"><?=HTML::escape($myUser['motto'])?></textarea>
+			<span class="help-block" id="help-motto"></span>
+		</div>
+	</div>
+
+	<div class="form-group">
+    	<div class="col-sm-offset-2 col-sm-3">
+	      <p class="form-control-static"><strong><?= UOJLocale::get('change avatar help') ?></strong></p>
+	    </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"><?= UOJLocale::get('submit') ?></button>
+		</div>
+	</div>
+</form>
+
+<script type="text/javascript">
+	function validateUpdatePost() {
+		var ok = true;
+		ok &= getFormErrorAndShowHelp('email', validateEmail);
+		ok &= getFormErrorAndShowHelp('old_password', validatePassword);
+
+		if ($('#input-password').val().length > 0)
+			ok &= getFormErrorAndShowHelp('password', validateSettingPassword);
+		if ($('#input-qq').val().length > 0)
+			ok &= getFormErrorAndShowHelp('qq', validateQQ);
+		ok &= getFormErrorAndShowHelp('motto', validateMotto);
+		return ok;
+	}
+	function submitUpdatePost() {
+		if (!validateUpdatePost())
+			return;
+		$.post('/user/modify-profile', {
+			change   : '',
+			etag     : $('#input-email').val().length,
+			ptag     : $('#input-password').val().length,
+			Qtag     : $('#input-qq').val().length,
+			email    : $('#input-email').val(),
+			password : md5($('#input-password').val(), "<?= getPasswordClientSalt() ?>"),
+			old_password : md5($('#input-old_password').val(), "<?= getPasswordClientSalt() ?>"),
+			qq       : $('#input-qq').val(),
+			sex      : $('#input-sex').val(),
+			motto    : $('#input-motto').val()
+		}, function(msg) {
+			if (msg == 'ok') {
+				BootstrapDialog.show({
+					title   : '修改成功',
+					message : '用户信息修改成功',
+					type    : BootstrapDialog.TYPE_SUCCESS,
+					buttons : [{
+						label: '好的',
+						action: function(dialog) {
+							dialog.close();
+						}
+					}],
+					onhidden : function(dialog) {
+						window.location.href = '/user/profile/<?=$myUser['username']?>';
+					}
+				});
+			} else {
+				BootstrapDialog.show({
+					title   : '修改失败',
+					message : msg,
+					type    : BootstrapDialog.TYPE_DANGER,
+					buttons: [{
+						label: '好的',
+						action: function(dialog) {
+							dialog.close();
+						}
+					}],
+				});
+			}
+		});
+	}
+	$(document).ready(function(){$('#form-update').submit(function(e) {submitUpdatePost();e.preventDefault();});
+	});
+</script>
+<?php echoUOJPageFooter() ?>
+

+ 68 - 0
app/controllers/click_zan.php

@@ -0,0 +1,68 @@
+<?php
+	function validateZan() {
+		if (!validateUInt($_POST['id'])) {
+			return false;
+		}
+		if (!validateInt($_POST['delta'])) {
+			return false;
+		}
+		if ($_POST['delta'] != 1 && $_POST['delta'] != -1) {
+			return false;
+		}
+		if ($_POST['type'] != 'B' && $_POST['type'] != 'BC' && $_POST['type'] != 'P' && $_POST['type'] != 'C') {
+			return false;
+		}
+		return true;
+	}
+	if (!validateZan()) {
+		die('<div class="text-danger">failed</div>');
+	}
+	if ($myUser == null) {
+		die('<div class="text-danger">please <a href="'.HTML::url('/login').'">log in</a></div>');
+	}
+	
+	$id = $_POST['id'];
+	$delta = $_POST['delta'];
+	$type = $_POST['type'];
+	
+	switch ($type) {
+		case 'B':
+			$table_name = 'blogs';
+			break;
+		case 'BC':
+			$table_name = 'blogs_comments';
+			break;
+		case 'P':
+			$table_name = 'problems';
+			break;
+		case 'C':
+			$table_name = 'contests';
+			break;
+	}
+	
+	$cur = queryZanVal($id, $type, $myUser);
+	
+	if ($cur != $delta) {
+		$row = DB::selectFirst("select zan from $table_name where id = $id");
+		if ($row == null) {
+			die('<div class="text-danger">failed</div>');
+		}
+		$cur += $delta;
+		if ($cur == 0) {
+			DB::query("delete from click_zans where username = '{$myUser['username']}' and type = '$type' and target_id = $id");
+		} elseif ($cur != $delta) {
+			DB::query("update click_zans set val = '$cur' where username = '{$myUser['username']}' and type = '$type' and target_id = $id");
+		} else {
+			DB::query("insert into click_zans (username, type, target_id, val) values ('{$myUser['username']}', '$type', $id, $cur)");
+		}
+		$cnt = $row['zan'] + $delta;
+		DB::query("update $table_name set zan = $cnt where id = $id");
+	} else {
+		$row = DB::selectFirst("select zan from $table_name where id = $id");
+		if ($row == null) {
+			die('<div class="text-danger">failed</div>');
+		}
+		$cnt = $row['zan'];
+	}
+?>
+<?= getClickZanBlock($type, $id, $cnt, $cur) ?>

+ 77 - 0
app/controllers/cloud.php

@@ -0,0 +1,77 @@
+<?php
+	requirePHPLib('form');
+?>
+<?php echoUOJPageHeader('数据中心') ?>
+
+<div class="row shadow my-4">
+	<div class="col-12 p-4">
+		<h3>数据中心</h3>
+	</div>
+	<div class="col-6 p-4">
+		<input type="file" id="fileinput">
+	</div>
+	<div class="col-6 text-right p-4">
+		<span id="errormsg" class="text-danger"></span>
+		<button type="button" onclick="upload()" class="btn btn-primary">上传</button>
+	</div>
+</div>
+
+<script>
+let file = null, starttime = null, uploaded = 0, lasttime = null;
+
+document.getElementById('fileinput').onchange = (e) => file = e.target.files[0];
+
+function prettysize(i)
+{
+	return i;
+}
+
+function update_progress(curr, tot) {
+	let errmsg = document.getElementById('errormsg');
+	errmsg.innerHTML = '进度:' + prettysize(curr) + ' / ' + prettysize(tot);
+}
+
+async function upload() {
+	let errmsg = document.getElementById('errormsg');
+
+	if (!file) {
+		errmsg.innerHTML = '未选择文件';
+		return;
+	}
+
+	const HOST = 'https://103.205.253.104:56170';
+	const SYN = HOST + '/syn',
+		UPLOAD = HOST + '/upload',
+		FIN = HOST + '/fin';
+
+	var blksize = 1048576 * 4;
+	var syndata = new FormData();
+	syndata.append('size', file.size);
+
+	fetch(SYN, { method: 'POST', body: syndata }).then((res) => {
+		res.json().then(async (data) => {
+			if (data.status != 'ok') {
+				errmsg.innerHTML = '失败:' + data.msg;
+				return null;
+			}
+
+			var ssid = data['ssid'];
+			for (var i = 0; i < file.size; i += blksize) {
+				var data = new FormData();
+				data.append('chunk', file.slice(i, Math.min(file.size, i + blksize)));
+				data.append('ssid', ssid);
+				await fetch(UPLOAD, { method: 'POST', body: data });
+				update_progress(i, file.size);
+			}
+
+			var findata = new FormData();
+			findata.append('ssid', ssid);
+			await fetch(FIN, { method: 'POST', body: findata });
+
+			errmsg.textContent = '完成';
+		})});
+	}
+
+</script>
+
+<?php echoUOJPageFooter() ?>

+ 556 - 0
app/controllers/contest_inside.php

@@ -0,0 +1,556 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!validateUInt($_GET['id']) || !($contest = queryContest($_GET['id']))) {
+		become404Page();
+	}
+	genMoreContestInfo($contest);
+
+	if (!hasContestPermission(Auth::user(), $contest)) {
+		if ($contest['cur_progress'] == CONTEST_NOT_STARTED) {
+			header("Location: /contest/{$contest['id']}/register");
+			die();
+		} elseif ($contest['cur_progress'] == CONTEST_IN_PROGRESS) {
+			if ($myUser == null || !hasRegistered(Auth::user(), $contest)) {
+				becomeMsgPage("<h1>比赛正在进行中</h1><p>很遗憾,您尚未报名。比赛结束后再来看吧~</p>");
+			}
+		}
+	}
+	
+	if (isset($_GET['tab'])) {
+		$cur_tab = $_GET['tab'];
+	} else {
+		$cur_tab = 'dashboard';
+	}
+	
+	$tabs_info = array(
+		'dashboard' => array(
+			'name' => UOJLocale::get('contests::contest dashboard'),
+			'url' => "/contest/{$contest['id']}"
+		),
+		'submissions' => array(
+			'name' => UOJLocale::get('contests::contest submissions'),
+			'url' => "/contest/{$contest['id']}/submissions"
+		),
+		'standings' => array(
+			'name' => UOJLocale::get('contests::contest standings'),
+			'url' => "/contest/{$contest['id']}/standings"
+		)
+	);
+	
+	if (hasContestPermission(Auth::user(), $contest)) {
+		$tabs_info['backstage'] = array(
+			'name' => UOJLocale::get('contests::contest backstage'),
+			'url' => "/contest/{$contest['id']}/backstage"
+		);
+	}
+	
+	if (!isset($tabs_info[$cur_tab])) {
+		become404Page();
+	}
+	
+	if (isset($_POST['check_notice'])) {
+		$result = DB::query("select * from contests_notice where contest_id = '${contest['id']}' order by time desc limit 10");
+		$ch = array();
+		$flag = false;
+		try {
+			while ($row = DB::fetch($result)) {
+				if (new DateTime($row['time']) > new DateTime($_POST['last_time'])) {
+					$ch[] = $row['title'].': '.$row['content'];
+				}
+			}
+		} catch (Exception $e) {
+		}
+		global $myUser;
+		$result=DB::query("select * from contests_asks where contest_id='${contest['id']}' and username='${myUser['username']}' order by reply_time desc limit 10");
+		try {
+			while ($row = DB::fetch($result)) {
+				if (new DateTime($row['reply_time']) > new DateTime($_POST['last_time'])) {
+					$ch[] = $row['question'].': '.$row['answer'];
+				}
+			}
+		} catch (Exception $e) {
+		}
+		if ($ch) {
+			die(json_encode(array('msg' => $ch, 'time' => UOJTime::$time_now_str)));
+		} else {
+			die(json_encode(array('time' => UOJTime::$time_now_str)));
+		}
+	}
+	
+	if (isSuperUser($myUser)) {
+		if (CONTEST_PENDING_FINAL_TEST <= $contest['cur_progress'] && $contest['cur_progress'] <= CONTEST_TESTING) {
+			$start_test_form = new UOJForm('start_test');
+			$start_test_form->handle = function() {
+				global $contest;
+				$result = DB::query("select id, problem_id, content from submissions where contest_id = {$contest['id']}");
+				while ($submission = DB::fetch($result, MYSQLI_ASSOC)) {
+					if (!isset($contest['extra_config']["problem_{$submission['problem_id']}"])) {
+						$content = json_decode($submission['content'], true);
+						if (isset($content['final_test_config'])) {
+							$content['config'] = $content['final_test_config'];
+							unset($content['final_test_config']);
+						}
+						if (isset($content['first_test_config'])) {
+							unset($content['first_test_config']);
+						}
+						$esc_content = DB::escape(json_encode($content));
+						DB::update("update submissions set judge_time = NULL, result = '', score = NULL, status = 'Waiting Rejudge', content = '$esc_content' where id = {$submission['id']}");
+					}
+				}
+				DB::query("update contests set status = 'testing' where id = {$contest['id']}");
+			};
+			$start_test_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+			$start_test_form->submit_button_config['smart_confirm'] = '';
+			$start_test_form->submit_button_config['text'] = '重测所有提交';
+			$start_test_form->runAtServer();
+		}
+		if ($contest['cur_progress'] >= CONTEST_PENDING_FINAL_TEST) {
+			$publish_result_form = new UOJForm('publish_result');
+			$publish_result_form->handle = function() {
+				// time config
+				set_time_limit(0);
+				ignore_user_abort(true);
+
+				global $contest;
+				$contest_data = queryContestData($contest);
+				calcStandings($contest, $contest_data, $score, $standings, true);
+				if (!isset($contest['extra_config']['unrated'])) {
+					$rating_k = isset($contest['extra_config']['rating_k']) ? $contest['extra_config']['rating_k'] : 400;
+					$ratings = calcRating($standings, $rating_k);
+				} else {
+					$ratings = array();
+					for ($i = 0; $i < count($standings); $i++) {
+						$ratings[$i] = $standings[$i][2][1];
+					}
+				}
+
+				for ($i = 0; $i < count($standings); $i++) {
+					$user = queryUser($standings[$i][2][0]);
+					$change = $ratings[$i] - $user['rating'];
+					$user_link = getUserLink($user['username']);
+
+					if ($change != 0) {
+						$tail = '<strong style="color:red">' . ($change > 0 ? '+' : '') . $change . '</strong>';
+						$content = <<<EOD
+<p>${user_link} 您好:</p>
+<p class="indent2">您在 <a href="/contest/{$contest['id']}">{$contest['name']}</a> 这场比赛后的Rating变化为${tail},当前Rating为 <strong style="color:red">{$ratings[$i]}</strong>。</p>
+EOD;
+					} else {
+						$content = <<<EOD
+<p>${user_link} 您好:</p>
+<p class="indent2">您在 <a href="/contest/{$contest['id']}">{$contest['name']}</a> 这场比赛后Rating没有变化。当前Rating为 <strong style="color:red">{$ratings[$i]}</strong>。</p>
+EOD;
+					}
+					sendSystemMsg($user['username'], 'Rating变化通知', $content);
+					DB::query("update user_info set rating = {$ratings[$i]} where username = '{$standings[$i][2][0]}'");
+					DB::query("update contests_registrants set rank = {$standings[$i][3]} where contest_id = {$contest['id']} and username = '{$standings[$i][2][0]}'");
+				}
+				DB::query("update contests set status = 'finished' where id = {$contest['id']}");
+			};
+			$publish_result_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+			$publish_result_form->submit_button_config['smart_confirm'] = '';
+			$publish_result_form->submit_button_config['text'] = '公布成绩';
+			
+			$publish_result_form->runAtServer();
+		}
+	}
+	
+	if ($cur_tab == 'dashboard') {
+		if ($contest['cur_progress'] <= CONTEST_IN_PROGRESS) {
+			$post_question = new UOJForm('post_question');
+			$post_question->addVTextArea('qcontent', '问题', '', 
+				function($content) {
+					if (!Auth::check()) {
+						return '您尚未登录';
+					}
+					if (!$content || strlen($content) == 0) {
+						return '问题不能为空';
+					}
+					if (strlen($content) > 140 * 4) {
+						return '问题太长';
+					}
+					return '';
+				},
+				null
+			);
+			$post_question->handle = function() {
+				global $contest;
+				$content = DB::escape($_POST['qcontent']);
+				$username = Auth::id();
+				DB::query("insert into contests_asks (contest_id, question, username, post_time, is_hidden) values ('{$contest['id']}', '$content', '$username', now(), 1)");
+			};
+			$post_question->runAtServer();
+		} else {
+			$post_question = null;
+		}
+	} elseif ($cur_tab == 'backstage') {
+		if (isSuperUser(Auth::user())) {
+			$post_notice = new UOJForm('post_notice');
+			$post_notice->addInput('title', 'text', '标题', '',
+				function($title) {
+					if (!$title) {
+						return '标题不能为空';
+					}
+					return '';
+				},
+				null
+			);
+			$post_notice->addTextArea('content', '正文', '', 
+				function($content) {
+					if (!$content) {
+						return '公告不能为空';
+					}
+					return '';
+				},
+				null
+			);
+			$post_notice->handle = function() {
+				global $contest;
+				$title = DB::escape($_POST['title']);
+				$content = DB::escape($_POST['content']);
+				DB::insert("insert into contests_notice (contest_id, title, content, time) values ('{$contest['id']}', '$title', '$content', now())");
+			};
+			$post_notice->runAtServer();
+		} else {
+			$post_notice = null;
+		}
+		
+		if (hasContestPermission(Auth::user(), $contest)) {
+			$reply_question = new UOJForm('reply_question');
+			$reply_question->addHidden('rid', '0',
+				function($id) {
+					global $contest;
+				    
+					if (!validateUInt($id)) {
+						return '无效ID';
+					}
+					$q = DB::selectFirst("select * from contests_asks where id = $id");
+					if ($q['contest_id'] != $contest['id']) {
+						return '无效ID';
+					}
+					return '';
+				},
+				null
+			);
+			$reply_question->addVSelect('rtype', [
+				'public' => '公开',
+				'private' => '非公开',
+				'statement' => '请仔细阅读题面(非公开)',
+				'no_comment' => '无可奉告(非公开)',
+				'no_play' => '请认真比赛(非公开)',
+			], '回复类型', 'private');
+			$reply_question->addVTextArea('rcontent', '回复', '', 
+				function($content) {
+					if (!Auth::check()) {
+						return '您尚未登录';
+					}
+					switch ($_POST['rtype']) {
+				    	case 'public':
+				    	case 'private':
+				    		if (strlen($content) == 0) {
+				    			return '回复不能为空';
+				    		}
+							break;
+				    }
+					return '';
+				},
+				null
+			);
+			$reply_question->handle = function() {
+				global $contest;
+				$content = DB::escape($_POST['rcontent']);
+				$is_hidden = 1;
+				switch ($_POST['rtype']) {
+					case 'statement':
+						$content = '请仔细阅读题面';
+						break;
+					case 'no_comment':
+						$content = '无可奉告 ╮(╯▽╰)╭ ';
+						break;
+					case 'no_play':
+						$content = '请认真比赛 ( ̄口 ̄)!!';
+						break;
+					case 'public':
+						$is_hidden = 0;
+						break;
+					default:
+						break;
+				}
+				DB::update("update contests_asks set answer = '$content', reply_time = now(), is_hidden = {$is_hidden} where id = {$_POST['rid']}");
+			};
+			$reply_question->runAtServer();
+		} else {
+			$reply_question = null;
+		}
+	}
+	
+	function echoDashboard() {
+		global $contest, $post_notice, $post_question, $reply_question;
+		
+		$myname = Auth::id();
+		$contest_problems = DB::selectAll("select contests_problems.problem_id, best_ac_submissions.submission_id from contests_problems left join best_ac_submissions on contests_problems.problem_id = best_ac_submissions.problem_id and submitter = '{$myname}' where contest_id = {$contest['id']} order by contests_problems.problem_id asc");
+		
+		for ($i = 0; $i < count($contest_problems); $i++) {
+			$contest_problems[$i]['problem'] = queryProblemBrief($contest_problems[$i]['problem_id']);
+		}
+		
+		$contest_notice = DB::selectAll("select * from contests_notice where contest_id = {$contest['id']} order by time desc");
+		
+		if (Auth::check()) {
+			$my_questions = DB::selectAll("select * from contests_asks where contest_id = {$contest['id']} and username = '{$myname}' order by post_time desc");
+			$my_questions_pag = new Paginator([
+				'data' => $my_questions
+			]);
+		} else {
+			$my_questions_pag = null;
+		}
+		
+		$others_questions_pag = new Paginator([
+			'col_names' => array('*'),
+			'table_name' => 'contests_asks',
+			'cond' => "contest_id = {$contest['id']} and username != '{$myname}' and is_hidden = 0",
+			'tail' => 'order by reply_time desc',
+			'page_len' => 10
+		]);
+		
+		uojIncludeView('contest-dashboard', [
+			'contest' => $contest,
+			'contest_notice' => $contest_notice,
+			'contest_problems' => $contest_problems,
+			'post_question' => $post_question,
+			'my_questions_pag' => $my_questions_pag,
+			'others_questions_pag' => $others_questions_pag
+		]);
+	}
+	
+	function echoBackstage() {
+		global $contest, $post_notice, $reply_question;
+		
+		$questions_pag = new Paginator([
+			'col_names' => array('*'),
+			'table_name' => 'contests_asks',
+			'cond' => "contest_id = {$contest['id']}",
+			'tail' => 'order by post_time desc',
+			'page_len' => 50
+		]);
+		
+		if ($contest['cur_progress'] < CONTEST_TESTING) {
+			$contest_data = queryContestData($contest, ['pre_final' => true]);
+			calcStandings($contest, $contest_data, $score, $standings);
+			
+			$standings_data = [
+				'contest' => $contest,
+				'standings' => $standings,
+				'score' => $score,
+				'contest_data' => $contest_data
+			];
+		} else {
+			$standings_data = null;
+		}
+		
+		uojIncludeView('contest-backstage', [
+			'contest' => $contest,
+			'post_notice' => $post_notice,
+			'reply_question' => $reply_question,
+			'questions_pag' => $questions_pag,
+			'standings_data' => $standings_data
+		]);
+	}
+	
+	function echoMySubmissions() {
+		global $contest, $myUser;
+
+		$show_all_submissions_status = Cookie::get('show_all_submissions') !== null ? 'checked="checked" ' : '';
+		$show_all_submissions = UOJLocale::get('contests::show all submissions');
+		echo <<<EOD
+			<div class="checkbox text-right">
+				<label for="input-show_all_submissions"><input type="checkbox" id="input-show_all_submissions" $show_all_submissions_status/> $show_all_submissions</label>
+			</div>
+			<script type="text/javascript">
+				$('#input-show_all_submissions').click(function() {
+					if (this.checked) {
+						$.cookie('show_all_submissions', '');
+					} else {
+						$.removeCookie('show_all_submissions');
+					}
+					location.reload();
+				});
+			</script>
+EOD;
+		if (Cookie::get('show_all_submissions') !== null) {
+			echoSubmissionsList("contest_id = {$contest['id']}", 'order by id desc', array('judge_time_hidden' => ''), $myUser);
+		} else {
+			echoSubmissionsList("submitter = '{$myUser['username']}' and contest_id = {$contest['id']}", 'order by id desc', array('judge_time_hidden' => ''), $myUser);
+		}
+	}
+	
+	function echoStandings() {
+		global $contest;
+		
+		$contest_data = queryContestData($contest);
+		calcStandings($contest, $contest_data, $score, $standings);
+		
+		if ($contest['cur_progress'] >= CONTEST_FINISHED && hasContestPermission(Auth::user(), $contest)) {
+			echo <<<EOD
+				<div>
+					<a class="btn btn-info" href="/contest/{$contest['id']}/export_standings">下载排名</a>
+				</div>
+			EOD;
+		}
+		
+		uojIncludeView('contest-standings', [
+			'contest' => $contest,
+			'standings' => $standings,
+			'score' => $score,
+			'contest_data' => $contest_data
+		]);
+	}
+	
+	function echoContestCountdown() {
+		global $contest;
+		$rest_second = $contest['end_time']->getTimestamp() - UOJTime::$time_now->getTimestamp();
+		$time_str = UOJTime::$time_now_str;
+		$contest_ends_in = UOJLocale::get('contests::contest ends in');
+		echo <<<EOD
+ 		<div class="card border-info">
+ 			<div class="card-header bg-info">
+ 				<h3 class="card-title">$contest_ends_in</h3>
+ 			</div>
+ 			<div class="card-body text-center countdown" data-rest="$rest_second"></div>
+ 		</div>
+		<script type="text/javascript">
+			checkContestNotice({$contest['id']}, '$time_str');
+		</script>
+EOD;
+	}
+	
+	function echoContestJudgeProgress() {
+		global $contest;
+		if ($contest['cur_progress'] < CONTEST_TESTING) {
+			$rop = 0;
+			$title = UOJLocale::get('contests::contest pending final test');
+		} else {
+			$total = DB::selectCount("select count(*) from submissions where contest_id = {$contest['id']}");
+			$n_judged = DB::selectCount("select count(*) from submissions where contest_id = {$contest['id']} and status = 'Judged'");
+			$rop = $total == 0 ? 100 : (int)($n_judged / $total * 100);
+			$title = UOJLocale::get('contests::contest final testing');
+		}
+		echo <<<EOD
+ 		<div class="card border-info">
+ 			<div class="card-header bg-info">
+ 				<h3 class="card-title">$title</h3>
+ 			</div>
+ 			<div class="card-body">
+				<div class="progress bot-buffer-no">
+					<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="$rop" aria-valuemin="0" aria-valuemax="100" style="width: {$rop}%; min-width: 20px;">{$rop}%</div>
+				</div>
+			</div>
+ 		</div>
+EOD;
+	}
+	
+	function echoContestFinished() {
+		$title = UOJLocale::get('contests::contest ended');
+		echo <<<EOD
+ 		<div class="card border-info">
+ 			<div class="card-header bg-info">
+ 				<h3 class="card-title">$title</h3>
+ 			</div>
+ 		</div>
+EOD;
+	}
+	
+	$page_header = HTML::stripTags($contest['name']) . ' - ';
+?>
+<?php echoUOJPageHeader(HTML::stripTags($contest['name']) . ' - ' . $tabs_info[$cur_tab]['name'] . ' - ' . UOJLocale::get('contests::contest')) ?>
+<div class="text-center">
+	<h1><?= $contest['name'] ?></h1>
+	<?= getClickZanBlock('C', $contest['id'], $contest['zan']) ?>
+</div>
+<div class="row">
+	<?php if ($cur_tab == 'standings'): ?>
+	<div class="col-sm-12">
+	<?php else: ?>
+	<div class="col-sm-9">
+	<?php endif ?>
+		<?= HTML::tablist($tabs_info, $cur_tab) ?>
+		<div class="top-buffer-md">
+		<?php
+			if ($cur_tab == 'dashboard') {
+				echoDashboard();
+			} elseif ($cur_tab == 'submissions') {
+				echoMySubmissions();
+			} elseif ($cur_tab == 'standings') {
+				echoStandings();
+			} elseif ($cur_tab == 'backstage') {
+				echoBackstage();
+			}
+		?>
+		</div>
+	</div>
+	
+	<?php if ($cur_tab == 'standings'): ?>
+	<div class="col-sm-12">
+		<hr />
+	</div>
+	<?php endif ?>
+
+	<div class="col-sm-3">
+		<?php
+			if ($contest['cur_progress'] <= CONTEST_IN_PROGRESS) {
+				echoContestCountdown();
+			} elseif ($contest['cur_progress'] <= CONTEST_TESTING) {
+				echoContestJudgeProgress();
+			} else {
+				echoContestFinished();
+			}
+		?>
+		<?php if ($cur_tab == 'standings'): ?>
+	</div>
+	<div class="col-sm-3">
+	<?php endif ?>
+	<?php if (!isset($contest['extra_config']['contest_type']) || $contest['extra_config']['contest_type']=='OI'):?>
+	<p>此次比赛为OI赛制。</p>
+	<p><strong>注意:比赛时只显示测样例的结果。</strong></p>
+	<?php elseif ($contest['extra_config']['contest_type']=='IOI'):?>
+	<p>此次比赛为IOI赛制。</p>
+	<p><strong>注意:比赛时显示测试所有数据的结果,但无法看到详细信息。</strong></p>
+	<?php endif?>
+	
+		<a href="/contest/<?=$contest['id']?>/registrants" class="btn btn-info btn-block"><?= UOJLocale::get('contests::contest registrants') ?></a>
+		<?php if (isSuperUser($myUser)): ?>
+		<a href="/contest/<?=$contest['id']?>/manage" class="btn btn-primary btn-block">管理</a>
+		<?php if (isset($start_test_form)): ?>
+		<div class="top-buffer-sm">
+			<?php $start_test_form->printHTML(); ?>
+		</div>
+		<?php endif ?>
+		<?php if (isset($publish_result_form)): ?>
+		<div class="top-buffer-sm">
+			<?php $publish_result_form->printHTML(); ?>
+		</div>
+		<?php endif ?>
+		<?php endif ?>
+	
+		<?php if ($contest['extra_config']['links']) { ?>
+			<?php if ($cur_tab == 'standings'): ?>
+	</div>
+	<div class="col-sm-3">
+		<div class="card border-info">
+		<?php else: ?>
+		<div class="card border-info top-buffer-lg">
+		<?php endif ?>
+			<div class="card-header bg-info">
+				<h3 class="card-title">比赛资料</h3>
+			</div>
+			<div class="list-group">
+			<?php foreach ($contest['extra_config']['links'] as $link) { ?>
+				<a href="/blogs/<?=$link[1]?>" class="list-group-item"><?=$link[0]?></a>
+			<?php } ?>
+			</div>
+		</div>
+		<?php } ?>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 285 - 0
app/controllers/contest_manage.php

@@ -0,0 +1,285 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!validateUInt($_GET['id']) || !($contest = queryContest($_GET['id']))) {
+		become404Page();
+	}
+	genMoreContestInfo($contest);
+	
+	if (!isSuperUser($myUser)) {
+		become403Page();
+	}
+	
+	$time_form = new UOJForm('time');
+	$time_form->addInput(
+		'name', 'text', '比赛标题', $contest['name'],
+		function($str) {
+			return '';
+		},
+		null
+	);
+	$time_form->addInput(
+		'start_time', 'text', '开始时间', $contest['start_time_str'],
+		function($str, &$vdata) {
+			try {
+				$vdata['start_time'] = new DateTime($str);
+			} catch (Exception $e) {
+				return '无效时间格式';
+			}
+			return '';
+		},
+		null
+	);
+	$time_form->addInput(
+		'last_min', 'text', '时长(单位:分钟)', $contest['last_min'],
+		function($str) {
+			return !validateUInt($str) ? '必须为一个整数' : '';
+		},
+		null
+	);
+	$time_form->handle = function(&$vdata) {
+		global $contest;
+		$start_time_str = $vdata['start_time']->format('Y-m-d H:i:s');
+		
+		$purifier = HTML::pruifier();
+		
+		$esc_name = $_POST['name'];
+		$esc_name = $purifier->purify($esc_name);
+		$esc_name = DB::escape($esc_name);
+		
+		DB::update("update contests set start_time = '$start_time_str', last_min = {$_POST['last_min']}, name = '$esc_name' where id = {$contest['id']}");
+	};
+	
+	$managers_form = newAddDelCmdForm('managers',
+		function($username) {
+			if (!validateUsername($username) || !queryUser($username)) {
+				return "不存在名为{$username}的用户";
+			}
+			return '';
+		},
+		function($type, $username) {
+			global $contest;
+			if ($type == '+') {
+				DB::query("insert into contests_permissions (contest_id, username) values (${contest['id']}, '$username')");
+			} elseif ($type == '-') {
+				DB::query("delete from contests_permissions where contest_id = ${contest['id']} and username = '$username'");
+			}
+		}
+	);
+	
+	$problems_form = newAddDelCmdForm('problems',
+		function($cmd) {
+			if (!preg_match('/^(\d+)\s*(\[\S+\])?$/', $cmd, $matches)) {
+				return "无效题号";
+			}
+			$problem_id = $matches[1];
+			if (!validateUInt($problem_id) || !($problem = queryProblemBrief($problem_id))) {
+				return "不存在题号为{$problem_id}的题";
+			}
+			if (!hasProblemCtrlPermission(Auth::user(), $problem)) {
+				return "无权添加题号为{$problem_id}的题";
+			}
+			return '';
+		},
+		function($type, $cmd) {
+			global $contest;
+			
+			if (!preg_match('/^(\d+)\s*(\[\S+\])?$/', $cmd, $matches)) {
+				return "无效题号";
+			}
+			
+			$problem_id = $matches[1];
+			
+			if ($type == '+') {
+				DB::insert("insert into contests_problems (contest_id, problem_id) values ({$contest['id']}, '$problem_id')");
+			} elseif ($type == '-') {
+				DB::delete("delete from contests_problems where contest_id = {$contest['id']} and problem_id = '$problem_id'");
+			}
+			
+			if (isset($matches[2])) {
+				switch ($matches[2]) {
+					case '[sample]':
+						unset($contest['extra_config']["problem_$problem_id"]);
+						break;
+					case '[full]':
+						$contest['extra_config']["problem_$problem_id"] = 'full';
+						break;
+					case '[no-details]':
+						$contest['extra_config']["problem_$problem_id"] = 'no-details';
+						break;
+				}
+				$esc_extra_config = json_encode($contest['extra_config']);
+				$esc_extra_config = DB::escape($esc_extra_config);
+				DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
+			}
+		}
+	);
+	
+	if (isSuperUser($myUser)) {
+		$rating_k_form = new UOJForm('rating_k');
+		$rating_k_form->addInput('rating_k', 'text', 'rating 变化上限', isset($contest['extra_config']['rating_k']) ? $contest['extra_config']['rating_k'] : 400,
+			function ($x) {
+				if (!validateUInt($x) || $x < 1 || $x > 1000) {
+					return '不合法的上限';
+				}
+				return '';
+			},
+			null
+		);
+		$rating_k_form->handle = function() {
+			global $contest;
+			$contest['extra_config']['rating_k'] = $_POST['rating_k'];
+			$esc_extra_config = json_encode($contest['extra_config']);
+			$esc_extra_config = DB::escape($esc_extra_config);
+			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
+		};
+		$rating_k_form->runAtServer();
+		
+		$rated_form = new UOJForm('rated');
+		$rated_form->handle = function() {
+			global $contest;
+			if (isset($contest['extra_config']['unrated'])) {
+				unset($contest['extra_config']['unrated']);
+			} else {
+				$contest['extra_config']['unrated'] = '';
+			}
+			$esc_extra_config = json_encode($contest['extra_config']);
+			$esc_extra_config = DB::escape($esc_extra_config);
+			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
+		};
+		$rated_form->submit_button_config['class_str'] = 'btn btn-warning btn-block';
+		$rated_form->submit_button_config['text'] = isset($contest['extra_config']['unrated']) ? '设置比赛为rated' : '设置比赛为unrated';
+		$rated_form->submit_button_config['smart_confirm'] = '';
+	
+		$rated_form->runAtServer();
+		
+		$version_form = new UOJForm('version');
+		$version_form->addInput('standings_version', 'text', '排名版本', $contest['extra_config']['standings_version'],
+			function ($x) {
+				if (!validateUInt($x) || $x < 1 || $x > 2) {
+					return '不是合法的版本号';
+				}
+				return '';
+			},
+			null
+		);
+		$version_form->handle = function() {
+			global $contest;
+			$contest['extra_config']['standings_version'] = $_POST['standings_version'];
+			$esc_extra_config = json_encode($contest['extra_config']);
+			$esc_extra_config = DB::escape($esc_extra_config);
+			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
+		};
+		$version_form->runAtServer();
+
+		$contest_type_form = new UOJForm('contest_type');
+		$contest_type_form->addInput('contest_type', 'text', '赛制', $contest['extra_config']['contest_type'],
+			function ($x) {
+				if ($x != 'OI' && $x != 'ACM' && $x != 'IOI') {
+					return '不是合法的赛制名';
+				}
+				return '';
+			},
+			null
+		);
+		$contest_type_form->handle = function() {
+			global $contest;
+			$contest['extra_config']['contest_type'] = $_POST['contest_type'];
+			$esc_extra_config = json_encode($contest['extra_config']);
+			$esc_extra_config = DB::escape($esc_extra_config);
+			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
+		};
+		$contest_type_form->runAtServer();
+	}
+	
+	$time_form->runAtServer();
+	$managers_form->runAtServer();
+	$problems_form->runAtServer();
+?>
+<?php echoUOJPageHeader(HTML::stripTags($contest['name']) . ' - 比赛管理') ?>
+<h1 class="page-header" align="center"><?=$contest['name']?> 管理</h1>
+<ul class="nav nav-tabs mb-3" role="tablist">
+	<li class="nav-item"><a class="nav-link active" href="#tab-time" role="tab" data-toggle="tab">比赛时间</a></li>
+	<li class="nav-item"><a class="nav-link" href="#tab-managers" role="tab" data-toggle="tab">管理者</a></li>
+	<li class="nav-item"><a class="nav-link" href="#tab-problems" role="tab" data-toggle="tab">试题</a></li>
+	<?php if (isSuperUser($myUser)): ?>
+	<li class="nav-item"><a class="nav-link" href="#tab-others" role="tab" data-toggle="tab">其它</a></li>
+	<?php endif ?>
+	<li class="nav-item"><a class="nav-link" href="/contest/<?=$contest['id']?>" role="tab">返回</a></li>
+</ul>
+<div class="tab-content top-buffer-sm">
+	<div class="tab-pane active" id="tab-time">
+		<?php $time_form->printHTML(); ?>
+	</div>
+	
+	<div class="tab-pane" id="tab-managers">
+		<table class="table table-hover">
+			<thead>
+				<tr>
+					<th>#</th>
+					<th>用户名</th>
+				</tr>
+			</thead>
+			<tbody>
+<?php
+	$row_id = 0;
+	$result = DB::query("select username from contests_permissions where contest_id = {$contest['id']}");
+	while ($row = DB::fetch($result, MYSQLI_ASSOC)) {
+		$row_id++;
+		echo '<tr>', '<td>', $row_id, '</td>', '<td>', getUserLink($row['username']), '</td>', '</tr>';
+	}
+?>
+			</tbody>
+		</table>
+		<p class="text-center">命令格式:命令一行一个,+mike表示把mike加入管理者,-mike表示把mike从管理者中移除</p>
+		<?php $managers_form->printHTML(); ?>
+	</div>
+	
+	<div class="tab-pane" id="tab-problems">
+		<table class="table table-hover">
+			<thead>
+				<tr>
+					<th>#</th>
+					<th>试题名</th>
+				</tr>
+			</thead>
+			<tbody>
+<?php
+	$result = DB::query("select problem_id from contests_problems where contest_id = ${contest['id']} order by problem_id asc");
+	while ($row = DB::fetch($result, MYSQLI_ASSOC)) {
+		$problem = queryProblemBrief($row['problem_id']);
+		$problem_config_str = isset($contest['extra_config']["problem_{$problem['id']}"]) ? $contest['extra_config']["problem_{$problem['id']}"] : 'sample';
+		echo '<tr>', '<td>', $problem['id'], '</td>', '<td>', getProblemLink($problem), ' ', "[$problem_config_str]", '</td>', '</tr>';
+	}
+?>
+			</tbody>
+		</table>
+		<p class="text-center">命令格式:命令一行一个,+233表示把题号为233的试题加入比赛,-233表示把题号为233的试题从比赛中移除</p>
+		<?php $problems_form->printHTML(); ?>
+	</div>
+	<?php if (isSuperUser($myUser)): ?>
+	<div class="tab-pane" id="tab-others">
+		<div class="row">
+			<div class="col-sm-12">
+				<h3>Rating控制</h3>
+				<div class="row">
+					<div class="col-sm-3">
+						<?php $rated_form->printHTML(); ?>
+					</div>
+				</div>
+				<div class="top-buffer-sm"></div>
+				<?php $rating_k_form->printHTML(); ?>
+			</div>
+			<div class="col-sm-12 top-buffer-sm">
+				<h3>版本控制</h3>
+				<?php $version_form->printHTML(); ?>
+			</div>
+			<div class="col-sm-12 top-buffer-sm">
+				<h3>赛制</h3>
+				<?php $contest_type_form->printHTML(); ?>
+			</div>
+		</div>
+	</div>
+	<?php endif ?>
+</div>
+<?php echoUOJPageFooter() ?>

+ 111 - 0
app/controllers/contest_members.php

@@ -0,0 +1,111 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!validateUInt($_GET['id']) || !($contest = queryContest($_GET['id']))) {
+		become404Page();
+	}
+	genMoreContestInfo($contest);
+	
+	$has_contest_permission = hasContestPermission($myUser, $contest);
+	$show_ip = $has_contest_permission;
+	
+	if ($contest['cur_progress'] == CONTEST_NOT_STARTED) {
+		$iHasRegistered = $myUser != null && hasRegistered($myUser, $contest);
+	
+		if ($iHasRegistered) {
+			$unregister_form = new UOJForm('unregister');
+			$unregister_form->handle = function() {
+				global $myUser, $contest;
+				DB::query("delete from contests_registrants where username = '{$myUser['username']}' and contest_id = {$contest['id']}");
+				updateContestPlayerNum($contest);
+			};
+			$unregister_form->submit_button_config['class_str'] = 'btn btn-danger btn-xs';
+			$unregister_form->submit_button_config['text'] = '取消报名';
+			$unregister_form->succ_href = "/contests";
+		
+			$unregister_form->runAtServer();
+		}
+		
+		if ($has_contest_permission) {
+			$pre_rating_form = new UOJForm('pre_rating');
+			$pre_rating_form->handle = function() {
+				global $contest;
+				foreach (DB::selectAll("select * from contests_registrants where contest_id = {$contest['id']}") as $reg) {
+					$user = queryUser($reg['username']);
+					DB::update("update contests_registrants set user_rating = {$user['rating']} where contest_id = {$contest['id']} and username = '{$user['username']}'");
+				}
+			};
+			$pre_rating_form->submit_button_config['align'] = 'right';
+			$pre_rating_form->submit_button_config['class_str'] = 'btn btn-warning';
+			$pre_rating_form->submit_button_config['text'] = '重新计算参赛前的 rating';
+			$pre_rating_form->submit_button_config['smart_confirm'] = '';
+			
+			$pre_rating_form->runAtServer();
+		}
+	}
+?>
+<?php echoUOJPageHeader(HTML::stripTags($contest['name']) . ' - ' . UOJLocale::get('contests::contest registrants')) ?>
+
+<h1 class="text-center"><?= $contest['name'] ?></h1>
+<?php if ($contest['cur_progress'] == CONTEST_NOT_STARTED): ?>
+	<?php if ($iHasRegistered): ?>
+		<div class="float-right">
+			<?php $unregister_form->printHTML(); ?>
+		</div>
+		<div><a style="color:green">已报名</a></div>
+	<?php else: ?>
+		<div>当前尚未报名,您可以<a style="color:red" href="/contest/<?= $contest['id'] ?>/register">报名</a>。</div>
+	<?php endif ?>
+<div class="top-buffer-sm"></div>
+<?php endif ?>
+
+<?php
+	if ($show_ip) {
+		$header_row = '<tr><th>#</th><th>'.UOJLocale::get('username').'</th><th>remote_addr</th><th>rating</th></tr>';
+	
+		$ip_owner = array();
+		foreach (DB::selectAll("select * from contests_registrants where contest_id = {$contest['id']} order by user_rating asc") as $reg) {
+			$user = queryUser($reg['username']);
+			$ip_owner[$user['remote_addr']] = $reg['username'];
+		}
+	} else {
+		$header_row = '<tr><th>#</th><th>'.UOJLocale::get('username').'</th><th>rating</th></tr>';
+	}
+	
+	echoLongTable(array('*'), 'contests_registrants', "contest_id = {$contest['id']}", 'order by user_rating desc',
+		$header_row,
+		function($contest, $num) {
+			global $myUser;
+			global $show_ip, $ip_owner;
+			
+			$user = queryUser($contest['username']);
+			$user_link = getUserLink($contest['username'], $contest['user_rating']);
+			if (!$show_ip) {
+				echo '<tr>';
+			} else {
+#				if ($ip_owner[$user['remote_addr']] != $user['username']) {
+#					echo '<tr class="table-danger">';
+#				} else {
+					echo '<tr>';
+#				}
+			}
+			echo '<td>'.$num.'</td>';
+			echo '<td>'.$user_link.'</td>';
+			if ($show_ip) {
+				echo '<td>'.$user['remote_addr'].'</td>';
+			}
+			echo '<td>'.$contest['user_rating'].'</td>';
+			echo '</tr>';
+		},
+		array('page_len' => 100,
+			'get_row_index' => '',
+			'print_after_table' => function() {
+				global $pre_rating_form;
+				if (isset($pre_rating_form)) {
+					$pre_rating_form->printHTML();
+				}
+			}
+		)
+	);
+?>
+<?php echoUOJPageFooter() ?>

+ 37 - 0
app/controllers/contest_registration.php

@@ -0,0 +1,37 @@
+<?php
+	requirePHPLib('form');
+	if (!validateUInt($_GET['id']) || !($contest = queryContest($_GET['id']))) {
+		become404Page();
+	}
+	genMoreContestInfo($contest);
+	
+	if ($myUser == null) {
+		redirectToLogin();
+	} elseif (hasContestPermission($myUser, $contest) || hasRegistered($myUser, $contest) || $contest['cur_progress'] != CONTEST_NOT_STARTED) {
+		redirectTo('/contests');
+	}
+	
+	$register_form = new UOJForm('register');
+	$register_form->handle = function() {
+		global $myUser, $contest;
+		DB::query("insert into contests_registrants (username, user_rating, contest_id, has_participated) values ('{$myUser['username']}', {$myUser['rating']}, {$contest['id']}, 0)");
+		updateContestPlayerNum($contest);
+	};
+	$register_form->submit_button_config['class_str'] = 'btn btn-primary';
+	$register_form->submit_button_config['text'] = '报名比赛';
+	$register_form->succ_href = "/contests";
+	
+	$register_form->runAtServer();
+?>
+<?php echoUOJPageHeader(HTML::stripTags($contest['name']) . ' - 报名') ?>
+<h1 class="page-header">比赛规则</h1>
+<ul>
+	<li>比赛报名后不算正式参赛,报名后进了比赛页面也不算参赛,<strong>看了题目才算正式参赛</strong>。如果未正式参赛则不算rating。</li>
+	<li>比赛中途可以提交,若同一题有多次提交按<strong>最后一次不是Compile Error的提交</strong>算成绩。(其实UOJ会自动无视你所有Compile Error的提交当作没看见)</li>
+	<li>比赛中途提交后,可以看到<strong>测样例</strong>的结果。(若为提交答案题则对于每个测试点,该测试点有分则该测试点为满分)</li>
+	<li>比赛结束后会进行最终测试,最终测试后的排名为最终排名。</li>
+	<li>比赛排名按分数为第一关键字,完成题目的总时间为第二关键字。完成题目的总时间等于完成每道题所花时间之和(无视掉爆零的题目)。</li>
+	<li>请遵守比赛规则,一位选手在一场比赛内不得报名多个账号,选手之间不能交流或者抄袭代码,如果被检测到将以0分处理或者封禁。</li>
+</ul>
+<?php $register_form->printHTML(); ?>
+<?php echoUOJPageFooter() ?>

+ 103 - 0
app/controllers/contests.php

@@ -0,0 +1,103 @@
+<?php
+	requirePHPLib('form');
+	
+	$upcoming_contest_name = null;
+	$upcoming_contest_href = null;
+	$rest_second = 1000000;
+	function echoContest($contest) {
+		global $myUser, $upcoming_contest_name, $upcoming_contest_href, $rest_second;
+		
+		$contest_name_link = <<<EOD
+<a href="/contest/{$contest['id']}">{$contest['name']}</a>
+EOD;
+		genMoreContestInfo($contest);
+		if ($contest['cur_progress'] == CONTEST_NOT_STARTED) {
+			$cur_rest_second = $contest['start_time']->getTimestamp() - UOJTime::$time_now->getTimestamp();
+			if ($cur_rest_second < $rest_second) {
+				$upcoming_contest_name = $contest['name'];
+				$upcoming_contest_href = "/contest/{$contest['id']}";
+				$rest_second = $cur_rest_second;
+			}
+			if ($myUser != null && hasRegistered($myUser, $contest)) {
+				$contest_name_link .= '<sup><a style="color:green">'.UOJLocale::get('contests::registered').'</a></sup>';
+			} else {
+				$contest_name_link .= '<sup><a style="color:red" href="/contest/'.$contest['id'].'/register">'.UOJLocale::get('contests::register').'</a></sup>';
+			}
+		} elseif ($contest['cur_progress'] == CONTEST_IN_PROGRESS) {
+			$contest_name_link .= '<sup><a style="color:blue" href="/contest/'.$contest['id'].'">'.UOJLocale::get('contests::in progress').'</a></sup>';
+		} elseif ($contest['cur_progress'] == CONTEST_PENDING_FINAL_TEST) {
+			$contest_name_link .= '<sup><a style="color:blue" href="/contest/'.$contest['id'].'">'.UOJLocale::get('contests::pending final test').'</a></sup>';
+		} elseif ($contest['cur_progress'] == CONTEST_TESTING) {
+			$contest_name_link .= '<sup><a style="color:blue" href="/contest/'.$contest['id'].'">'.UOJLocale::get('contests::final testing').'</a></sup>';
+		} elseif ($contest['cur_progress'] == CONTEST_FINISHED) {
+			$contest_name_link .= '<sup><a style="color:grey" href="/contest/'.$contest['id'].'/standings">'.UOJLocale::get('contests::ended').'</a></sup>';
+		}
+		
+		$last_hour = round($contest['last_min'] / 60, 2);
+		
+		$click_zan_block = getClickZanBlock('C', $contest['id'], $contest['zan']);
+		echo '<tr>';
+		echo '<td>', $contest_name_link, '</td>';
+		echo '<td>', '<a href="'.HTML::timeanddate_url($contest['start_time'], array('duration' => $contest['last_min'])).'">'.$contest['start_time_str'].'</a>', '</td>';
+		echo '<td>', UOJLocale::get('hours', $last_hour), '</td>';
+		echo '<td>', '<a href="/contest/'.$contest['id'].'/registrants"><span class="glyphicon glyphicon-user"></span> &times;'.$contest['player_num'].'</a>', '</td>';
+		echo '<td>', '<div class="text-center">'.$click_zan_block.'</div>', '</td>';
+		echo '</tr>';
+	}
+?>
+
+<?php echoUOJPageHeader(UOJLocale::get('contests')) ?>
+
+<div class="row">
+<h4><?= UOJLocale::get('contests::current or upcoming contests') ?></h4>
+<?php
+	$table_header = '';
+	$table_header .= '<tr>';
+	$table_header .= '<th>'.UOJLocale::get('contests::contest name').'</th>';
+	$table_header .= '<th style="width:15em;">'.UOJLocale::get('contests::start time').'</th>';
+	$table_header .= '<th style="width:100px;">'.UOJLocale::get('contests::duration').'</th>';
+	$table_header .= '<th style="width:100px;">'.UOJLocale::get('contests::the number of registrants').'</th>';
+	$table_header .= '<th style="width:220px;">'.UOJLocale::get('appraisal').'</th>';
+	$table_header .= '</tr>';
+	echoLongTable(array('*'), 'contests', "status != 'finished'", 'order by start_time asc, id asc', $table_header,
+		echoContest,
+		array('page_len' => 100)
+	);
+
+	if ($rest_second <= 86400) {
+		$notification = json_encode($upcoming_contest_name . " 已经开始了。是否要跳转到比赛页面?");
+		echo <<<EOD
+<div class="w-100 text-center bot-buffer-lg">
+<div class="text-warning">$upcoming_contest_name 倒计时</div>
+<div id="contest-countdown"></div>
+<script type="text/javascript">
+$('#contest-countdown').countdown($rest_second, function() {
+	if (confirm($notification)) {
+		window.location.href = "$upcoming_contest_href";
+	}
+});
+</script>
+</div>
+EOD;
+	}
+?>
+</div>
+<div class="row">
+<h4><?= UOJLocale::get('contests::ended contests') ?></h4>
+<?php
+	echoLongTable(array('*'), 'contests', "status = 'finished'", 'order by start_time desc, id desc', $table_header,
+		echoContest,
+		array('page_len' => 100,
+			'print_after_table' => function() {
+				global $myUser;
+				if (isSuperUser($myUser)) {
+					echo '<div class="text-right">';
+					echo '<a href="/contest/new" class="btn btn-primary">'.UOJLocale::get('contests::add new contest').'</a>';
+					echo '</div>';
+				}
+			}
+		)
+	);
+?>
+</div>
+<?php echoUOJPageFooter() ?>

+ 49 - 0
app/controllers/datum.php

@@ -0,0 +1,49 @@
+<?php
+	requirePHPLib('form');
+
+	if (!isSuperUser($myUser))
+		become403Page();
+	
+	if (!validateUInt($_GET['id']) || !($datum = queryDatum($_GET['id'])))
+		become403Page();
+	$submission = querySubmission($hack['submission_id']);
+	$problem = queryProblemBrief($submission['problem_id']);
+
+?>
+<?php
+	$REQUIRE_LIB['hljs'] = "";
+?>
+<?php echoUOJPageHeader('请求详情 #' . $datum['id']) ?>
+
+<?php
+	echoDatumListOnlyOne($datum);
+
+	echo '<div class="card border-info mb-3">';
+	echo '<div class="card-header bg-info">';
+	echo '<h4 class="card-title">流</h4>';
+	echo '</div>';
+	echo '<div class="card-body"><pre><code class="sh_python hljs language-python">';
+	echo queryDatumStream($datum['id'])['stream'];
+	echo '</code></pre></div>';
+	echo '</div>';
+
+	echo '<div class="card border-info mb-3">';
+	echo '<div class="card-header bg-info">';
+	echo '<h4 class="card-title">gen.py</h4>';
+	echo '</div>';
+	echo '<div class="card-body"><pre><code class="sh_python hljs language-python">';
+	echo $datum['generator'];
+	echo '</code></pre></div>';
+	echo '</div>';
+
+	echo '<div class="card border-info mb-3">';
+	echo '<div class="card-header bg-info">';
+	echo '<h4 class="card-title">std</h4>';
+	echo '</div>';
+	echo '<div class="card-body"><pre><code class="sh_cpp hljs language-cpp">';
+	echo $datum['std'];
+	echo '</code></pre></div>';
+	echo '</div>';
+?>
+
+<?php echoUOJPageFooter() ?>

+ 83 - 0
app/controllers/datums.php

@@ -0,0 +1,83 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+
+	if ($myUser == null || !isSuperUser($myUser))
+		become403Page();
+	$cancel_form = new UOJForm('retry');
+	$cancel_form->addInput('id', 'text', '重试请求', '',
+		function ($id) {
+			if (!validateUInt($id)) return 'ID 不合法';
+			$u = queryDatumBrief($id);
+			if (!$u) return '请求不存在';
+			if ($u['status'] == 'Generating') return '请求处理中';
+			return '';
+		}, null);
+	$cancel_form->handle = function() {
+		$id = $_POST['id'];
+		DB::update("update datum_requests set status = 'Waiting' where id = $id");
+	};
+	$cancel_form->submit_button_config['class_str'] = 'btn btn-danger';
+	$cancel_form->submit_button_config['align'] = 'offset';
+	$cancel_form->submit_button_config['text'] = '重试';
+	$cancel_form->runAtServer();
+
+	$retry_form = new UOJForm('retry');
+	$retry_form->addInput('id', 'text', '重试请求', '',
+		function ($id) {
+			if (!validateUInt($id)) return 'ID 不合法';
+			$u = queryDatumBrief($id);
+			if (!$u) return '请求不存在';
+			if ($u['status'] == 'Generating') return '请求处理中';
+			return '';
+		}, null);
+	$retry_form->handle = function() {
+		$id = $_POST['id'];
+		DB::update("update datum_requests set request_time = now() where id = $id");
+		DB::update("update datum_requests set finish_time = NULL where id = $id");
+		DB::update("update datum_requests set status = 'Waiting' where id = $id");
+	};
+	$retry_form->submit_button_config['class_str'] = 'btn btn-danger';
+	$retry_form->submit_button_config['align'] = 'offset';
+	$retry_form->submit_button_config['text'] = '重试';
+	$retry_form->runAtServer();
+
+	$new_datum_form = new UOJForm('datum');
+	$new_datum_form->addInput('pid', 'text', '题目编号', '',
+		function ($pid) {
+			if (!validateUInt($pid)) return '题号不合法';
+			if (!queryProblemBrief($pid)) return '题目不存在';
+			return '';
+		}, null);
+	$new_datum_form->addInput('timelim', 'text', '时间限制', '',
+		function ($pid) {
+			if (!validateUInt($pid)) return '时间限制不合法';
+			return '';
+		}, null);
+	$new_datum_form->addInput('memlim', 'text', '空间限制', '',
+		function ($pid) {
+			if (!validateUInt($pid)) return '空间限制不合法';
+			return '';
+		}, null);
+	$new_datum_form->addSourceCodeInput('std', '标程', array('C++17'));
+	$new_datum_form->handle = function() {
+		$std = '';
+		if ($_POST['std_upload_type'] == 'editor') $std = $_POST['std_editor'];
+		else $std = $_POST['std_file'];
+		$std = DB::escape($std);
+		$pid = $_POST['pid'];
+		$timelim = $_POST['timelim'];
+		$memlim = $_POST['memlim'];
+		DB::insert("insert into datum_requests (pid, request_time, timelimit, memlimit, status, std) values ($pid, now(), $timelim, $memlim, 'Waiting', '$std')");
+	};
+	$new_datum_form->runAtServer();
+?>
+<?php echoUOJPageHeader('自动数据生成') ?>
+<?php
+	echoDatumList();
+	echo '<h4>重试由大模型不稳定导致失败的请求</h4>';
+	$retry_form->printHTML();
+	echo '<h4>数据生成完毕后请手动点击校验配置并同步数据</h4>';
+	$new_datum_form->printHTML();
+?>
+<?php echoUOJPageFooter() ?>

+ 50 - 0
app/controllers/download.php

@@ -0,0 +1,50 @@
+<?php
+	requirePHPLib('judger');
+	switch ($_GET['type']) {
+		case 'problem':
+			if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
+				become404Page();
+			}
+			
+			$visible = isProblemVisibleToUser($problem, $myUser);
+			if (!$visible && $myUser != null) {
+				$result = DB::query("select contest_id from contests_problems where problem_id = {$_GET['id']}");
+				while (list($contest_id) = DB::fetch($result, MYSQLI_NUM)) {
+					$contest = queryContest($contest_id);
+					genMoreContestInfo($contest);
+					if ($contest['cur_progress'] != CONTEST_NOT_STARTED && hasRegistered($myUser, $contest) && queryContestProblemRank($contest, $problem)) {
+						$visible = true;
+					}
+				}
+			}
+			if (!$visible) {
+				become404Page();
+			}
+
+			if (!hasProblemCtrlPermission($myUser, $problem))
+				become403Page();
+
+			$id = $_GET['id'];
+			
+			$file_name = "/var/uoj_data/$id.zip";
+			$download_name = "problem_$id.zip";
+			break;
+		case 'testlib.h':
+			$file_name = "/opt/uoj/judger/uoj_judger/include/testlib.h";
+			$download_name = "testlib.h";
+			break;
+		default:
+			become404Page();
+	}
+	
+	$finfo = finfo_open(FILEINFO_MIME);
+	$mimetype = finfo_file($finfo, $file_name);
+	if ($mimetype === false) {
+		become404Page();
+	}
+	finfo_close($finfo);
+	
+	header("X-Sendfile: $file_name");
+	header("Content-type: $mimetype");
+	header("Content-Disposition: attachment; filename=$download_name");
+?>

+ 70 - 0
app/controllers/export_contest_standings_table.php

@@ -0,0 +1,70 @@
+<?php
+
+if (!validateUInt($_GET['id']) || !($contest = queryContest($_GET['id']))) {
+	become404Page();
+}
+genMoreContestInfo($contest);
+
+if (!hasContestPermission(Auth::user(), $contest)) {
+	becomeMsgPage('您没有权限下载此比赛的排名。');
+}
+if ($contest['cur_progress'] < CONTEST_FINISHED) {
+	becomeMsgPage('比赛尚未结束,无法下载排名。');
+}
+
+$contest_data = queryContestData($contest);
+calcStandings($contest, $contest_data, $score, $standings);
+
+function array2csv(array &$array) {
+	if (count($array) == 0) {
+		return null;
+	}
+
+	ob_start();
+
+	$df = fopen("php://output", 'w');
+
+	fputcsv($df, array_keys(reset($array)));
+
+	foreach ($array as $row) {
+		fputcsv($df, $row);
+	}
+
+	fclose($df);
+
+	return ob_get_clean();
+}
+
+$export_data = [];
+$csv_header = ['Rank', 'Username', 'Score', 'Penalty'];
+$n_problems = count($contest_data['problems']);
+
+for ($i = 0; $i < $n_problems; $i++) {
+	$csv_header[] = chr(ord('A') + $i);
+	$csv_header[] = chr(ord('A') + $i) . '_penalty';
+	$csv_header[] = chr(ord('A') + $i) . '_submission_id';
+}
+
+$export_data[] = $csv_header;
+
+// Convert data in $standings and $score to $export_data
+foreach ($standings as $rank => $row) {
+	// $row: rank, username, score, penalty
+	$res = [$rank + 1, $row[2][0], $row[0], $row[1]];
+	for ($i = 0; $i < $n_problems; $i++) {
+		// $score[$row[2][0]][$i]: score, penalty, submission_id
+		$res[] = isset($score[$row[2][0]][$i][0]) ? $score[$row[2][0]][$i][0] : "";
+		$res[] = isset($score[$row[2][0]][$i][1]) ? $score[$row[2][0]][$i][1] : "";
+		$res[] = isset($score[$row[2][0]][$i][2]) ? $score[$row[2][0]][$i][2] : "";
+	}
+	$export_data[] = $res;
+}
+
+$csv = array2csv($export_data);
+
+header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate");
+header("Last-Modified: {$now} GMT");
+header("Content-Type: text/csv");
+header("Content-Disposition: attachment;filename={$contest['id']}_standings.csv");
+
+die($csv);

+ 210 - 0
app/controllers/faq.php

@@ -0,0 +1,210 @@
+<?php
+	requireLib('hljs');
+	requireLib('mathjax');
+	echoUOJPageHeader(UOJLocale::get('help')) 
+?>
+<article>
+	<header>
+		<h2 class="page-header">常见问题及其解答(FAQ)</h2>
+	</header>
+	<section>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerOne" data-toggle="collapse" data-target="#collapseOne" style="cursor:pointer;">
+				<h5 class="mb-0">什么是<?= UOJConfig::$data['profile']['oj-name-short'] ?></h5>
+			</div>
+			<div id="collapseOne" class="collapse">
+				<div class="card-body">
+					<p>来了?坐,欢迎来到 <?= UOJConfig::$data['profile']['oj-name'] ?>。</p>
+					<p><img src="/images/utility/qpx_n/b37.gif" alt="小熊像超人一样飞" /></p>
+					<p>众所周知,信息学的题目一般形式为:给出XXXXX,要你提交一份源代码,输出XXXXX,然后时限若干秒,内存若干兆,数据若干组,每组数据与答案进行比较,不对就不给分。</p>
+					<p>看起来挺合理的,但是总是有意外。比如要求输出一个浮点数,与答案接近就满分。于是只好引入Special Judge来判断选手输出的正确性。</p>
+					<p>但是还是有意外,比如提交两个程序,一个压缩另一个解压;比如提交答案题只用提交文件;比如给出音乐要求识别乐器,达到90%的正确率就算满分……</p>
+					<p>这个时候UOJ出现了,于是<?= UOJConfig::$data['profile']['oj-name-short'] ?>就使用了这套系统。Universal的中文意思是通用,之所以称之为UOJ,因为我们所有题目从编译、运行到评分,都可以由出题人自定义。</p>
+					<p>如果你正在为没有地方测奇奇怪怪的题目而苦恼,那么你来对地方了。</p>
+					<p>当然了,<?= UOJConfig::$data['profile']['oj-name-short'] ?>对于传统题的评测也做了特别支持。平时做题时我很难容忍的地方就是数据出水了导致暴力得了好多分甚至过了,而出题人却委屈地说,总共才一百分,卡了这个暴力就不能卡另一个暴力,所以暴力过了就过了吧。</p>
+					<p>所以我们引入了Extra Tests和Hack机制。每道传统题的数据都分为Tests和Extra Tests,Tests满分100分,如果你通过了所有的Tests,那么就会为你测Extra Tests。如果过了Tests但没过Extra Tests那么倒扣3分变为97分。Extra Tests的来源,一个是这道题没什么人可能会错的边界情况可以放在里面,另一个就是各位平时做题的时候,如果发现错误算法AC了,可以使用hack将其卡掉,<?= UOJConfig::$data['profile']['oj-name-short'] ?>会自动加入Extra Tests并重测。我们无法阻止暴力高分的脚步,但是不让他得满分还是有心里安慰作用的~</p>
+					<p><?= UOJConfig::$data['profile']['oj-name-short'] ?>还有比赛功能可以承办比赛,赛制暂时只支持OI赛制。(不过你可以利用现有方案变相实现ACM赛制!)未来将支持更多种多样的赛制甚至自定义赛制。</p>
+					<p>目前<?= UOJConfig::$data['profile']['oj-name-short'] ?>刚刚起步,还有很多地方有待完善。想出题、想出比赛、发现BUG、发现槽点都可以联系我们,联系方式见下。</p>
+					<p>祝各位在<?= UOJConfig::$data['profile']['oj-name-short'] ?>玩得愉快!(求不虐萌萌哒服务器~求不虐萌萌哒测评机~!)</p>
+					<p><img src="/images/utility/qpx_n/b54.gif" alt="小熊抱抱" /></p>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerTwo" data-toggle="collapse" data-target="#collapseTwo" style="cursor:pointer;">
+				<h5 class="mb-0">注册后怎么上传头像</h5>
+			</div>
+			<div id="collapseTwo" class="collapse">
+				<div class="card-body">
+					<p><?= UOJConfig::$data['profile']['oj-name-short'] ?>不提供头像存储服务。每到一个网站都要上传一个头像挺烦的对不对?我们支持Gravatar,请使用Gravatar吧!Gravatar是一个全球的头像存储服务,你的头像将会与你的电子邮箱绑定。在各大网站比如各种Wordpress还有各种OJ比如Vijos、Contest Hunter上,只要你电子邮箱填对了,那么你的头像也就立即能显示了!</p>
+					<p>快使用Gravatar吧! Gravatar地址:<a href="https://cn.gravatar.com/">https://cn.gravatar.com/</a>。进去后注册个帐号然后与邮箱绑定并上传头像,就ok啦!</p>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerThree" data-toggle="collapse" data-target="#collapseThree" style="cursor:pointer;">
+				<h5 class="mb-0"><?= UOJConfig::$data['profile']['oj-name-short'] ?>的测评环境?</h5>
+			</div>
+			<div id="collapseThree" class="collapse">
+				<div class="card-body">
+					<p>默认的测评环境是 Ubuntu Linux 20.04 LTS x64。</p>
+					<p>C的编译器是 gcc 9.3.0,编译命令:<code>gcc code.c -o code -lm -O2 -DONLINE_JUDGE</code>。</p>
+					<p>C++的编译器是 g++ 9.3.0,编译命令:<code>g++ code.cpp -o code -lm -O2 -DONLINE_JUDGE</code>。如果选择C++11会在编译命令后面添加<code>-std=c++11</code>。</p>
+					<p>Java8的JDK版本是 openjdk 1.8.0_275,编译命令:<code>javac code.java</code>。</p>
+					<p>Java11的JDK版本是 openjdk 11.0.9,编译命令:<code>javac code.java</code>。</p>
+					<p>Pascal的编译器是 fpc 3.0.4,编译命令:<code>fpc code.pas -O2</code>。</p>
+					<p>Python会先编译为优化过的字节码<samp>.pyo</samp>文件。支持的Python版本分别为Python 2.7和3.8。</p>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerFour" data-toggle="collapse" data-target="#collapseFour" style="cursor:pointer;">
+				<h5 class="mb-0">各种评测状态的鸟语是什么意思?</h5>
+			</div>
+			<div id="collapseFour" class="collapse">
+				<div class="card-body">
+					<ul>
+						<li>Accepted: 答案正确。恭喜大佬,您通过了这道题。</li>
+						<li>Wrong Answer: 答案错误。仅仅通过样例数据的测试并不一定是正确答案,一定还有你没想到的地方。</li>
+						<li>Runtime Error: 运行时错误。像非法的内存访问,数组越界,指针漂移,调用禁用的系统函数都可能出现这类问题,请点击评测详情获得输出。</li>
+						<li>Time Limit Exceeded: 时间超限。请检查程序是否有死循环,或者应该有更快的计算方法。</li>
+						<li>Memory Limit Exceeded: 内存超限。数据可能需要压缩,或者您数组开太大了,请检查是否有内存泄露。</li>
+						<li>Output Limit Exceeded: 输出超限。你的输出居然比正确答案长了两倍!</li>
+						<li>Dangerous Syscalls: 危险系统调用,你是不是带了文件,或者使用了某些有意思的system函数?</li>
+						<li>Judgement Failed: 评测失败。可能是评测机抽风了,也可能是服务器正在睡觉;反正不一定是你的锅啦!</li>
+						<li>No Comment: 没有详情。评测机对您的程序无话可说,那么我们也不知道到底发生了什么...</li>
+					</ul>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerFive" data-toggle="collapse" data-target="#collapseFive" style="cursor:pointer;">
+				<h5 class="mb-0">递归 10<sup>7</sup> 层怎么没爆栈啊</h5>
+			</div>
+			<div id="collapseFive" class="collapse">
+				<div class="card-body">
+					<p>没错就是这样!除非是特殊情况,<?= UOJConfig::$data['profile']['oj-name-short'] ?>测评程序时的栈大小与该题的空间限制是相等的!</p>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerSix" data-toggle="collapse" data-target="#collapseSix" style="cursor:pointer;">
+				<h5 class="mb-0">我在本地/某某OJ上AC了,但在<?= UOJConfig::$data['profile']['oj-name-short'] ?>却过不了...这咋办?</h5>
+			</div>
+			<div id="collapseSix" class="collapse">
+				<div class="card-body">
+					<p>对于这类问题,我们在这里简单列一下可能原因:</p>
+					<ul>
+						<li>Linux中换行符是'\n'而windows中是'\r\n'(多一个字符)。有些数据在Windows下生成,而<?= UOJConfig::$data['profile']['oj-name-short'] ?>评测环境为Linux系统。这种情况在字符串输入中非常常见。</li>
+						<li>评测系统建立在Linux下,可能由于使用了Linux的保留字而出现CE,但在Windows下正常。</li>
+						<li>Linux对内存的访问控制更为严格,因此在Windows上可能正常运行的无效指针或数组下标访问越界,在评测系统上无法运行。</li>
+						<li>严重的内存泄露的问题很可能会引起系统的保护模块杀死你的进程。因此,凡是使用malloc(或calloc,realloc,new)分配而得的内存空间,请使用free(或delete)完全释放。</li>
+						<li>当然数据可能真的有问题。但是如果不止一个人通过了这道题,那最好不要怀疑是数据的锅。反之,可以立即联系我们上报!</li>
+					</ul>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerSeven" data-toggle="collapse" data-target="#collapseSeven" style="cursor:pointer;">
+				<h5 class="mb-0">博客使用指南</h5>
+			</div>
+			<div id="collapseSeven" class="collapse">
+				<div class="card-body">
+					<p><?= UOJConfig::$data['profile']['oj-name-short'] ?>博客使用的是Markdown。(好吧……好简陋的……好多功能还没写……)</p>
+					<p>(喂喂喂我们是OJ好吗……要那么完善的博客功能干啥呢……?)</p>
+					<p>其实我觉得Markdown不用教!一学就会!</p>
+					<p>(完蛋了……<?= UOJConfig::$data['profile']['oj-name-short'] ?>好像没有Markdown的语法高亮……= =……)</p>
+					<p>我就只介绍最基本的功能好了。其它的自己探索吧~比如<a href="http://wow.kuapp.com/markdown/">这里</a>。</p>
+					<!-- readmore -->
+					<p><code>**强调**</code> = <strong>强调</strong></p>
+					<hr /><p><code>*强调*</code> = <em>强调</em></p>
+					<hr /><p><code>[<?= UOJConfig::$data['profile']['oj-name-short'] ?>](<?= HTML::url('/') ?>)</code> = <a href="<?= HTML::url('/') ?>"><?= UOJConfig::$data['profile']['oj-name-short'] ?></a></p>
+					<hr /><p><code><?= HTML::url('/') ?></code> = <a href="http://<?= UOJConfig::$data['web']['main']['host'] ?>"><?= HTML::url('/') ?></a></p>
+					<hr /><p><code>![这个文字在图挂了的时候会显示](<?= HTML::url('/images/favicon.ico') ?>)</code> =
+					<img src="<?= HTML::url('/images/favicon.ico') ?>" alt="这个文字在图挂了的时候会显示" /></p>
+					<hr /><p><code>`rm orz`</code> = <code>rm orz</code></p>
+					<hr /><p><code>数学公式萌萌哒$(a + b)^2$萌萌哒</code> = 数学公式萌萌哒$(a + b)^2$萌萌哒</p>
+					<hr /><p><code>&lt;!-- readmore --&gt;</code> = 在外面看这篇博客时会到此为止然后显示一个“阅读更多”字样</p>
+					<hr /><p>来个更大的例子:</p>
+					<pre>
+					```c++
+					#include &lt;iostream&gt;
+					```
+
+					```c
+					#include &lt;stdio.h&gt;
+					```
+
+					```pascal
+					begin
+					```
+
+					```python
+					print '<?= UOJConfig::$data['profile']['oj-name-short'] ?>'
+					```
+
+					\begin{equation}
+					\frac{-b + \sqrt{b^2 - 4ac}}{2a}
+					\end{equation}
+
+					#一级标题
+					##二级标题
+					###三级标题
+					####四级标题
+					</pre>
+					<p>会转换为:</p>
+					<pre><code class="sh_cpp">#include &lt;iostream&gt;</code></pre>
+					<pre><code class="sh_c">#include &lt;stdio.h&gt;</code></pre>
+					<pre><code class="sh_pascal">begin</code></pre>
+					<pre><code class="sh_python">print '<?= UOJConfig::$data['profile']['oj-name-short'] ?>'</code></pre>
+					<p>\begin{equation}
+					\frac{-b + \sqrt{b^2 - 4ac}}{2a}
+					\end{equation}</p>
+					<h1>一级标题</h1>
+					<h2>二级标题</h2>
+					<h3>三级标题</h3>
+					<h4>四级标题</h4>
+					<hr /><p>还有一个很重要的事情,就是你很容易以为<?= UOJConfig::$data['profile']['oj-name-short'] ?>在吃换行……</p>
+					<p>那是因为跟LaTeX一样,你需要一个空行来分段。你可以粗略地认为两个换行会被替换成一换行。(当然不完全是这样,空行是用来分段的,段落还有间距啊行首空两格啊之类的属性)</p>
+					<p>唔……就介绍到这里吧。想要更详细的介绍上网搜搜吧~</p>
+					<p>(评论区是不可以用任何HTML滴~但是数学公式还是没问题滴)</p>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerEight" data-toggle="collapse" data-target="#collapseEight" style="cursor:pointer;">
+				<h5 class="mb-0">交互式类型的题怎么本地测试</h5>
+			</div>
+			<div id="collapseEight" class="collapse">
+				<div class="card-body">
+					<p>唔……好问题。交互式的题一般给了一个头文件要你include进来,以及一个实现接口的源文件grader。好像大家对多个源文件一起编译还不太熟悉。</p>
+					<p>对于C++:<code>g++ -o code grader.cpp code.cpp</code></p>
+					<p>对于C语言:<code>gcc -o code grader.c code.c</code></p>
+					<p>如果你是悲催的电脑盲,实在不会折腾没关系!你可以把grader的文件内容完整地粘贴到你的code的include语句之后,就可以了!</p>
+					<p>什么你是萌萌哒Pascal选手?一般来说都会给个grader,你需要写一个Pascal单元。这个grader会使用你的单元。所以你只需要把源文件取名为单元名 + <code>.pas</code>,然后:</p>
+					<p>对于Pascal语言:<code>fpc grader.pas</code></p>
+					<p>就可以啦!</p>
+				</div>
+			</div>
+		</div>
+		<div class="card my-1">
+			<div class="card-header collapsed" id="headerNine" data-toggle="collapse" data-target="#collapseNine" style="cursor:pointer;">
+				<h5 class="mb-0">联系方式</h5>
+			</div>
+			<div id="collapseNine" class="collapse">
+				<div class="card-body">
+					<p>如果你想出题、想办比赛、发现了BUG或者对网站有什么建议,可以通过下面的方式联系我们:</p>
+					<ul>
+						<li>私信联系<?= UOJConfig::$data['profile']['administrator'] ?>。</li>
+						<li>邮件联系<?= UOJConfig::$data['profile']['admin-email'] ?>。</li>
+						<?php if (UOJConfig::$data['profile']['QQ-group']!=''): ?>
+						<li>你也可以进QQ群水水,群号是<?= UOJConfig::$data['profile']['QQ-group'] ?>。</li>
+						<?php endif ?>
+					</ul>
+				</div>
+			</div>
+		</div>
+	</section>
+</article>
+
+<?php echoUOJPageFooter() ?>

+ 59 - 0
app/controllers/forgot_pw.php

@@ -0,0 +1,59 @@
+<?php
+	requirePHPLib('form');
+	
+	$forgot_form = new UOJForm('forgot');
+	$forgot_form->addInput('username', 'text', '用户名', '',
+		function($username, &$vdata) {
+			if (!validateUsername($username)) {
+				return '用户名不合法';
+			}
+			$vdata['user'] = queryUser($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
+<base target="_blank" />
+
+<p>{$user['username']}您好,</p>
+<p>您刚刚启用了{$oj_name_short}密码找回功能,请进入下面的链接重设您的密码:</p>
+<p><a href="$url">$url</a></p>
+<p>{$oj_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>
+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();
+?>
+<?php echoUOJPageHeader('找回密码') ?>
+<h2 class="page-header">找回密码</h2>
+<h4>请输入需要找回密码的用户名:</h4>
+<?php $forgot_form->printHTML(); ?>
+<?php echoUOJPageFooter() ?>

+ 90 - 0
app/controllers/group_info.php

@@ -0,0 +1,90 @@
+<?php
+	$username = $_GET['group'];
+	
+	requireLib('flot');
+?>
+<?php if (validateUsername($username) && ($user = queryUser($username))): ?>
+	<?php echoUOJPageHeader($user['username'] . ' - ' . '组信息') ?>
+	<?php
+	$email = $user['email'];
+	$esc_motto = HTML::escape($user['motto']);
+	$quota = printSize(queryGroupQuota($user));
+	$problemquota = queryGroupProblemQuota($user);
+	$creator = $user['childgroup'];
+	?>
+	<div class="card border-info">
+		<h5 class="card-header bg-info">组信息</h5>
+		<div class="card-body">
+			<div class="row mb-4">
+				<div class="col-md-4 order-md-9">
+					<img class="media-object img-thumbnail d-block mx-auto" alt="<?= $user['username'] ?> Avatar" height="250" width="250" src="<?= HTML::avatar_addr($user, 256) ?>" />
+				</div>
+				<div class="col-md-8 order-md-1">
+					<h2><span class="uoj-honor" data-rating="3000"><?= $user['username'] ?></span> <span><strong style="<?= $col_sex ?>"><?= $esc_sex ?></strong></span></h2>
+					<div class="list-group">
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">组昵称</h4>
+							<p class="list-group-item-text"><?= $email ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">组创建者</h4>
+							<p class="list-group-item-text"><?= getUserLink($creator) ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">组空间配额</h4>
+							<p class="list-group-item-text"><?= $quota ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">组题目配额</h4>
+							<p class="list-group-item-text"><?= $problemquota ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">组管理员</h4>
+							<p class="list-group-item-text">
+							<?php
+								$ctrls = explode(',', $user['ctrls']);
+								foreach ($ctrls as $i => $k)
+									if ($k != '') echo getUserLink($k) . ' ';
+							?>
+							</p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">组成员</h4>
+							<p class="list-group-item-text">
+							<?php
+								foreach (queryGroupMembers($user) as $i => $k)
+									echo getUserLink($k['username']) . ' ';
+							?>
+							</p>
+						</div>
+						<?php if (isSuperUser($myUser)): ?>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">register time</h4>
+							<p class="list-group-item-text"><?= $user['register_time'] ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">remote_addr</h4>
+							<p class="list-group-item-text"><?= $user['remote_addr'] ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">http_x_forwarded_for</h4>
+							<p class="list-group-item-text"><?= $user['http_x_forwarded_for'] ?></p>
+						</div>
+						<?php endif ?>
+					</div>
+				</div>
+			</div>	
+		</div>
+	</div>
+
+<?php else: ?>
+	<?php echoUOJPageHeader('不存在该用户' . ' - 用户信息') ?>
+	<div class="card border-danger">
+		<div class="card-header bg-danger">用户信息</div>
+		<div class="card-body">
+		<h4>不存在该用户</h4>
+		</div>
+	</div>
+<?php endif ?>
+
+<?php echoUOJPageFooter() ?>

+ 164 - 0
app/controllers/group_manage.php

@@ -0,0 +1,164 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	
+	if ($myUser == null) {
+		become403Page();
+	}
+	
+	$cur_tab = isset($_GET['tab']) ? $_GET['tab'] : 'info';
+	if (!isset($_GET['group']))
+		become404Page();
+	$groupname = $_GET['group'];
+	$group = queryUser($_GET['group']);
+
+	if (!hasGroupCtrlPermission($myUser, $group)) 
+		become403Page();
+
+	$tabs_info = array(
+		'info' => array(
+			'name' => '信息管理',
+			'url' => "/group-manage/$groupname/info"
+		),
+		'members' => array(
+			'name' => "成员管理",
+			'url' => "/group-manage/$groupname/members"
+		),
+		'problems' => array(
+			'name' => '题目管理',
+			'url' => "/group-manage/$groupname/problems"
+		),
+		'disk' => array(
+			'name' => '空间管理',
+			'url' => "/group-manage/$groupname/disk"
+		)
+	);
+	
+	$member_form = new UOJForm('member');
+	$member_form->addInput('username', 'text', '用户名', '',
+		function ($username) {
+			global $groupname;
+			if (!validateUsername($username)) {
+				return '用户名不合法';
+			}
+			$user = queryUser($username);
+			if (!$user) {
+				return '用户不存在';
+			}
+			if (!in_array($groupname, queryUserExtgroups($user)))
+				return '用户不属于该组';
+			return '';
+		},
+		null
+	);
+	$options = array(
+		'banneduser' => '踢出用户',
+		'normaluser' => '设为普通用户',
+		'superuser' => '设为超级用户'
+	);
+	$member_form->addSelect('op-type', $options, '操作类型', '');
+	$member_form->handle = function() {
+		global $member_form;
+		global $group;
+		
+		$username = $_POST['username'];
+		switch ($_POST['op-type']) {
+		case 'banneduser':
+			deattachUserFromGroup(queryUser($username), $group);
+			break;
+		case 'normaluser':
+			removeGroupPermission(queryUser($username), $group);
+			break;
+		case 'superuser':
+			giveGroupPermission(queryUser($username), $group);
+			break;
+		}
+	};
+	$member_form->runAtServer();
+		
+	$user_form = new UOJForm('user');
+	$user_form->addInput('usernamep', 'text', '用户名', '',
+		function ($username) {
+			global $groupname;
+			if (!validateUsername($username)) {
+				return '用户名不合法';
+			}
+			$user = queryUser($username);
+			if (!$user) {
+				return '用户不存在';
+			}
+			if (in_array($groupname, queryUserExtgroups($user)))
+				return '用户已经属于该组';
+			return '';
+		},
+		null
+	);
+	$user_form->handle = function() {
+		global $member_form;
+		global $group;
+		
+		$username = $_POST['usernamep'];
+		attachUserToGroup(queryUser($username), $group);
+	};
+	$user_form->runAtServer();
+			
+	$nick_form = new UOJForm('nick');
+	$nick_form->addInput('nick', 'text', '新昵称', '',
+		function ($username) {
+			return '';
+		},
+		null
+	);
+	$nick_form->handle = function() {
+		global $groupname;
+		$esc_nick = DB::escape($_POST['nick']);
+		DB::update("update user_info set email = '$esc_nick' where username = '$groupname'");
+	};
+
+	$nick_form->runAtServer();
+	if (!isset($tabs_info[$cur_tab])) {
+		become404Page();
+	}
+?>
+<?php
+	requireLib('hljs');
+	requireLib('morris');
+?>
+<?php echoUOJPageHeader('组管理') ?>
+<div class="row">
+	<div class="col-sm-3">
+		<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills flex-column') ?>
+	</div>
+	
+	<div class="col-sm-9">
+		<?php if ($cur_tab == 'info'): ?>
+			<div>
+				<h4>更改信息</h4>
+				<?php $nick_form->printHTML() ?>
+			</div>
+		<?php elseif ($cur_tab === 'members'): ?>
+			<div>
+				<h4>成员操作</h4>
+				<?php $member_form->printHTML() ?>
+				<h4>添加用户进组</h4>
+				<?php $user_form->printHTML() ?>
+				<h4><?php echo $groupname ?> 成员列表</h4>
+				<?php echoGroupMemberList($group) ?>
+			</div>
+		<?php elseif ($cur_tab == 'problems'): ?>
+			<div>
+				<h4><?php echo $groupname ?> 题目列表</h4>
+				<?php echoGroupProblemList($group) ?>
+			</div>
+		<?php elseif ($cur_tab == 'disk'): ?>
+			<div>
+				<h4>空间管理</h4>
+				<p>实际占用空间:<b class="text-danger">
+				<?php echo printSize(queryGroupTotalSize($group)) ?></b><br>
+				可用空间配额:<b class="text-danger"><?php echo printSize(queryGroupQuota($group)) ?></b></p>
+				<?php echoGroupProblemSizeList($group) ?>
+			</div>
+		<?php endif ?>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 105 - 0
app/controllers/groups_manage.php

@@ -0,0 +1,105 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	
+	if ($myUser == null) {
+		become403Page();
+	}
+	
+	$cur_tab = isset($_GET['tab']) ? $_GET['tab'] : 'my';
+	
+	$tabs_info = array(
+		'my' => array (
+			'name' => '我的组',
+			'url' => '/groups-manage/my'
+		),
+		'ctrls' => array(
+			'name' => '掌控组列表',
+			'url' => '/groups-manage/ctrls'
+		),
+		'belongs' => array(
+			'name' => '隶属组列表',
+			'url' => '/groups-manage/belongs'
+		)	
+	);
+
+	$new_group_form = new UOJForm('new-group');
+	$new_group_form->addInput('groupname', 'text', '组名称', '',
+		function ($name) {
+			global $myUser;
+			if ($myUser['childgroup'] != '')
+				return '你已经创建过组,不能再创建';
+			if (!validateUsername($name))
+				return '组名不合法';
+			if (substr($name, 0, 6) != 'group_')
+				return '组名必须以 group_ 开头';
+			$user = queryUser($name);
+			if ($user) return '组已存在';
+			return '';
+		}, null);
+	$new_group_form->addInput('nick', 'text', '组昵称', '',
+		function ($nick) {
+			return '';
+		}, null);
+	$new_group_form->handle = function() {
+		global $myUser;
+		$username = $myUser['username'];
+		$name = $_POST['groupname'];
+		$esc_nick = DB::escape($_POST['nick']);
+		DB::update("update user_info set childgroup = '$name' where username = '$username'");
+		DB::insert("insert into user_info (username, email, ac_num, ctrls, childgroup) 
+				values ('$name', '$esc_nick', 500, '$username', '$username')");
+		attachUserToGroup($myUser, queryUser($name));
+	};
+
+	$new_group_form->runAtServer();
+	
+	if (!isset($tabs_info[$cur_tab])) {
+		become404Page();
+	}
+?>
+<?php
+	requireLib('hljs');
+	requireLib('morris');
+?>
+<?php echoUOJPageHeader('组管理') ?>
+<div class="row">
+	<div class="col-sm-3">
+		<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills flex-column') ?>
+	</div>
+	
+	<div class="col-sm-9">
+		<?php if ($cur_tab == 'my'): ?>
+			<div>
+				<h4>我的组</h4>
+				<?php
+					if ($myUser['childgroup'] == '') {
+						echo '你尚未创建自己的组<br>';
+						echo '创建一个?<br><b class="text-danger">你只能创建一个组,且名称不可改变(昵称可以修改)。</b>';
+						
+						$new_group_form->printHTML();
+					}
+					else {
+						$grp = queryUser($myUser['childgroup']);
+						$grpnick = $grp['email'];
+						$grpname = $grp['username'];
+						echo "<div class='row mt-2'>";
+						echo "<div class='col-sm-6'><h4>$grpnick</h4></div>";
+						echo "<div class='col-sm-6 text-right'><a href='/group-manage/$grpname' class='btn btn-info'>管理</a></div></div>";
+					}
+				?>
+			</div>
+		<?php elseif ($cur_tab === 'ctrls'): ?>
+			<div>
+				<h4>掌控组列表</h4>
+				<?php echoControlGroupList($myUser) ?>
+			</div>
+		<?php elseif ($cur_tab == 'belongs'): ?>
+			<div>
+				<h4>隶属组列表</h4>
+				<?php echoBelongGroupList($myUser) ?>
+			</div>
+		<?php endif ?>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 86 - 0
app/controllers/hack.php

@@ -0,0 +1,86 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!validateUInt($_GET['id']) || !($hack = queryHack($_GET['id']))) {
+		become404Page();
+	}
+	$submission = querySubmission($hack['submission_id']);	
+	$problem = queryProblemBrief($submission['problem_id']);
+	$problem_extra_config = getProblemExtraConfig($problem);
+
+	if ($submission['contest_id']) {
+		$contest = queryContest($submission['contest_id']);
+		genMoreContestInfo($contest);
+	} else {
+		$contest = null;
+	}
+
+	if (!isHackVisibleToUser($hack, $problem, $myUser)) {
+		become403Page();
+	}
+	
+	if (isSuperUser($myUser)) {
+		$delete_form = new UOJForm('delete');
+		$delete_form->handle = function() {
+			global $hack;
+			DB::query("delete from hacks where id = {$hack['id']}");
+		};
+		$delete_form->submit_button_config['class_str'] = 'btn btn-danger';
+		$delete_form->submit_button_config['text'] = '删除此Hack';
+		$delete_form->submit_button_config['align'] = 'right';
+		$delete_form->submit_button_config['smart_confirm'] = '';
+		$delete_form->succ_href = "/hacks";
+		$delete_form->runAtServer();
+	}
+	
+	$should_show_content = hasViewPermission($problem_extra_config['view_content_type'], $myUser, $problem, $submission);
+	$should_show_all_details = hasViewPermission($problem_extra_config['view_all_details_type'], $myUser, $problem, $submission);
+	$should_show_details = hasViewPermission($problem_extra_config['view_details_type'], $myUser, $problem, $submission);
+	$should_show_details_to_me = isSuperUser($myUser);
+	if ($hack['success'] === null) {
+		$should_show_all_details = false;
+	}
+	if (!isSubmissionFullVisibleToUser($submission, $contest, $problem, $myUser)
+		|| !isHackFullVisibleToUser($hack, $contest, $problem, $myUser)) {
+		$should_show_content = $should_show_all_details = false;
+	}
+	
+	if ($should_show_all_details) {
+		$styler = new HackDetailsStyler();
+		if (!$should_show_details) {
+			$styler->fade_all_details = true;
+			$styler->show_small_tip = false;
+		}
+	}
+?>
+<?php
+	$REQUIRE_LIB['hljs'] = "";
+?>
+<?php echoUOJPageHeader(UOJLocale::get('problems::hack').' #'.$hack['id']) ?>
+
+<?php echoHackListOnlyOne($hack, array(), $myUser) ?>
+<?php if ($should_show_all_details): ?>
+	<div class="card border-info shadow my-4">
+		<div class="card-header bg-info">
+			<h4 class="card-title"><?= UOJLocale::get('details') ?></h4>
+		</div>
+		<div class="card-body">
+			<?php echoJudgementDetails($hack['details'], $styler, 'details') ?>
+			<?php if ($should_show_details_to_me): ?>
+				<?php if ($styler->fade_all_details): ?>
+					<hr />
+					<?php echoHackDetails($hack['details'], 'final_details') ?>
+				<?php endif ?>
+			<?php endif ?>
+		</div>
+	</div>
+<?php endif ?>
+<?php echoSubmissionsListOnlyOne($submission, array(), $myUser) ?>
+<?php if ($should_show_content): ?>
+	<?php echoSubmissionContent($submission, getProblemSubmissionRequirement($problem)) ?>
+<?php endif ?>
+
+<?php if (isset($delete_form)): ?>
+	<?php $delete_form->printHTML() ?>
+<?php endif ?>
+<?php echoUOJPageFooter() ?>

+ 101 - 0
app/controllers/hack_list.php

@@ -0,0 +1,101 @@
+<?php
+	$conds = array();
+
+	$q_problem_id = isset($_GET['problem_id']) && validateUInt($_GET['problem_id']) ? $_GET['problem_id'] : null;
+	$q_submission_id = isset($_GET['submission_id']) && validateUInt($_GET['submission_id']) ? $_GET['submission_id'] : null;
+	$q_hacker = isset($_GET['hacker']) && validateUsername($_GET['hacker']) ? $_GET['hacker'] : null;
+	$q_owner = isset($_GET['owner']) && validateUsername($_GET['owner']) ? $_GET['owner'] : null;
+	if ($q_problem_id != null) {
+		$conds[] = "problem_id = $q_problem_id";
+	}
+	if ($q_submission_id != null) {
+		$conds[] = "submission_id = $q_submission_id";
+	}
+	if ($q_hacker != null) {
+		$conds[] = "hacker = '$q_hacker'";
+	}
+	if ($q_owner != null) {
+		$conds[] = "owner = '$q_owner'";
+	}
+	
+	$selected_all = ' selected="selected"';
+	$selected_succ ='';
+	$selected_fail ='';
+	if (isset($_GET['status']) && validateUInt($_GET['status'])) {
+		if ($_GET['status'] == 1) {
+			$selected_all = '';
+			$selected_succ =' selected="selected"';
+			$conds[] = 'success = 1';
+		}
+		if ($_GET['status'] == 2) {
+			$selected_all = '';
+			$selected_fail = ' selected="selected"';
+			$conds[] = 'success = 0';
+		}
+	}
+	
+	if ($conds) {
+		$cond = join($conds, ' and ');
+	} else {
+		$cond = '1';
+	}
+	
+?>
+<?php echoUOJPageHeader(UOJLocale::get('hacks')) ?>
+<div class="d-none d-sm-block">
+	<?php if ($myUser != null): ?>
+	<div class="float-right">
+		<a href="/hacks?hacker=<?= $myUser['username'] ?>" class="btn btn-success btn-sm"><?= UOJLocale::get('problems::hacks by me') ?></a>
+		<a href="/hacks?owner=<?= $myUser['username'] ?>" class="btn btn-danger btn-sm"><?= UOJLocale::get('problems::hacks to me') ?></a>
+	</div>
+	<?php endif ?>
+	<form id="form-search" class="form-inline" role="form">
+		<div id="form-group-submission_id" class="form-group">
+			<label for="input-submission_id" class="control-label mx-2"><?= UOJLocale::get('problems::submission id') ?>:</label>
+			<input type="text" class="form-control input-sm" name="submission_id" id="input-submission_id" value="<?= $q_submission_id ?>" maxlength="6" style="width:5em" />
+		</div>
+		<div id="form-group-problem_id" class="form-group">
+			<label for="input-problem_id" class="control-label mx-2"><?= UOJLocale::get('problems::problem id') ?>:</label>
+			<input type="text" class="form-control input-sm" name="problem_id" id="input-problem_id" value="<?= $q_problem_id ?>" maxlength="4" style="width:4em" />
+		</div>
+		<div id="form-group-hacker" class="form-group">
+			<label for="input-hacker" class="control-label mx-2"><?= UOJLocale::get('problems::hacker') ?>:</label>
+			<input type="text" class="form-control input-sm" name="hacker" id="input-hacker" value="<?= $q_hacker ?>" maxlength="100" style="width:6em" />
+		</div>
+		<div id="form-group-owner" class="form-group">
+			<label for="input-owner" class="control-label mx-2"><?= UOJLocale::get('problems::owner') ?>:</label>
+			<input type="text" class="form-control input-sm" name="owner" id="input-owner" value="<?= $q_owner ?>" maxlength="100" style="width:6em" />
+		</div>
+		<div id="form-group-status" class="form-group">
+			<label for="input-status" class="control-label mx-2"><?= UOJLocale::get('problems::result') ?>:</label>
+			<select class="form-control input-sm" id="input-status" name="status" style="width:6em">
+				<option value=""<?= $selected_all?>>All</option>
+				<option value="1"<?= $selected_succ ?>>Success!</option>
+				<option value="2"<?= $selected_fail ?>>Failed.</option>
+			</select>
+		</div>
+		<button type="submit" id="submit-search" class="btn btn-secondary btn-sm ml-2"><?= UOJLocale::get('search') ?></button>
+	</form>
+	<script type="text/javascript">
+		$('#form-search').submit(function(e) {
+			e.preventDefault();
+			
+			url = '/hacks';
+			qs = [];
+			$(['submission_id', 'problem_id', 'hacker', 'owner', 'status']).each(function () {
+				if ($('#input-' + this).val()) {
+					qs.push(this + '=' + encodeURIComponent($('#input-' + this).val()));
+				}
+			});
+			if (qs.length > 0) {
+				url += '?' + qs.join('&');
+			}
+			location.href = url;
+		});
+	</script>
+	<div class="top-buffer-sm"></div>
+</div>
+<?php
+	echoHacksList($cond, 'order by id desc', array('judge_time_hidden' => ''), $myUser);
+?>
+<?php echoUOJPageFooter() ?>

+ 279 - 0
app/controllers/index.php

@@ -0,0 +1,279 @@
+<?php
+$blogs = DB::selectAll("select blogs.id, title, poster, post_time from important_blogs, blogs where is_hidden = 0 and important_blogs.blog_id = blogs.id order by level desc, important_blogs.blog_id desc limit 8");
+$topusers = DB::selectAll("select username, rating, qq, motto from user_info order by rating desc, username asc limit 12;");
+$topblogs = DB::selectAll("select id from blogs order by zan desc limit 10;");
+$songs_data = htmlspecialchars(json_encode(DB::selectAll("select * from songs;")), ENT_QUOTES, 'UTF-8');
+
+$submdata = DB::selectAll("select yearweek(submit_time, 1) as weeknr, date(submit_time - interval weekday(submit_time) day) as weekst, count(*) as cnt from submissions group by weeknr, weekst order by weekst;");
+$submjson = htmlspecialchars(json_encode($submdata), ENT_QUOTES, 'UTF-8');
+requireLib('flot');
+?>
+<?php echoUOJPageHeader(UOJConfig::$data['profile']['oj-name-short']) ?>
+<div id="songs-data" data-songs='<?php echo $songs_data; ?>'></div>
+<div class="row shadow my-4 wyoj-topcard">
+	<div class="card-body">
+		<div class="row">
+<!--			<div class="col-xs-6 col-sm-4 col-md-3">
+				<img class="media-object img-thumbnail" src="/images/logo.png" alt="Logo" />
+			</div>-->
+			<div class="col-sm-12 col-md-9">
+				<table class="table table-sm">
+<!--					<thead>
+						<tr>
+							<th style="width:60%"><?= UOJLocale::get('announcements') ?></th>
+							<th style="width:20%"></th>
+							<th style="width:20%"></th>
+						</tr>
+					</thead>-->
+				  	<tbody>
+					<?php $now_cnt = 0; ?>
+					<?php foreach ($blogs as $blog): ?>
+						<?php
+							$now_cnt++;
+							$new_tag = '';
+							if ((time() - strtotime($blog['post_time'])) / 3600 / 24 <= 7) {
+								$new_tag = '<sup style="color:red">&nbsp;new</sup>';
+							}
+						?>
+						<tr>
+							<td><a href="/blogs/<?= $blog['id'] ?>"><?= $blog['title'] ?></a><?= $new_tag ?></td>
+							<td>by <?= getUserLink($blog['poster']) ?></td>
+							<td><small><?= $blog['post_time'] ?></small></td>
+						</tr>
+					<?php endforeach ?>
+					<?php for ($i = $now_cnt + 1; $i <= 8; $i++): ?>
+						<tr><td colspan="233">&nbsp;</td></tr>
+					<?php endfor ?>
+						<tr><td class="text-right" colspan="233">
+							<span>本歌曲由 </span><span id="recommender"></span><span> 推荐</span>
+						</td></tr>
+						<tr><td class="text-right" colspan="233">
+							<a href="https://git.ryp.org.cn/wyoj/" class="text-info font-weight-bold">WyOJ 以及所有相关项目均为自由软件。</a>
+							<a href="/announcements"><?= UOJLocale::get('all the announcements') ?></a>
+						</td></tr>
+						<tr>
+							<td class="text-right" colspan="233">
+								<span class="text-danger font-weight-bold">声明:本网站属个人非经营性网站,仅用于学习交流,无任何盈利目的,与任何组织无关</span>
+								<a href="/certs-manage/songs">我要推歌</a>
+							</td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+			<div class="col-xs-6 col-sm-4 col-md-3">
+
+
+    
+	<div class="card" style="max-width:300px; width:100%; border-radius:0; ">
+        <div class="image-frame" style="aspect-ratio:1/1; position:relative;">
+            <img id="photo" src="" alt="" style="width:100%; height:100%; object-fit:cover; display:block; cursor:pointer; -webkit-tap-highlight-color:transparent;">
+        </div>
+        <div class="intro" style="padding:1.6rem 1.8rem 1.2rem; text-align:center; background:#ffffff;">
+            <p id="name" style="font-size:1.0rem; font-weight:800; letter-spacing:1px; margin-bottom:0.5rem; line-height:1.2; margin-block:0;"></p>
+            <p id="amb" style="font-size:0.7rem; font-weight:400; color:#4f5d69; margin-bottom:0.25rem; line-height:1.5; border-top:1px dashed #cdd9e3; padding-top:0.8rem; white-space:pre-wrap; margin-block:0;"></p>
+            <p id="writer" style="font-size:0.7rem; font-weight:400; color:#4f5d69; margin-bottom:0.25rem; line-height:1.5; white-space:pre-wrap; margin-block:0;"></p>
+        </div>
+        <div class="button-wrapper" style="padding:0.2rem 1.8rem 2rem 1.8rem; background:#ffffff;">
+            <style>
+                #randomBtn{
+                    display: inline-flex;
+                    align-items: center;
+                    justify-content: center;
+                    gap: 0.6rem;
+                    width: 100%;
+                    background: #1f2f3b;
+                    color: white;
+                    padding: 0.7rem 0;
+                    cursor: pointer;
+                    transition: all 0.2s ease;
+                    letter-spacing: 0.5px;
+                    border: 1px solid rgba(255, 255, 255, 0.2);
+                    backdrop-filter: blur(4px);
+                    font-family: inherit;
+                    line-height: 1;
+                }
+            </style>
+            <button id="randomBtn">换一首</button>
+        </div>
+    </div>
+    <script>
+        (function() {
+            let dataDiv = document.getElementById('songs-data');
+	    let library = JSON.parse(dataDiv.dataset.songs);
+
+            const photoImg = document.getElementById('photo');
+            const nameEl = document.getElementById('name');
+            const ambEl = document.getElementById('amb');
+            const writerEl = document.getElementById('writer');
+            const randomBtn = document.getElementById('randomBtn');
+            const recommenderEl = document.getElementById('recommender');
+
+            let lastIndex = -1;
+            let currentMusicLink = library[0].musiclink; 
+
+            photoImg.addEventListener('mousedown', function(e) {
+                this.style.opacity = '0.8';
+            });
+            photoImg.addEventListener('mouseup', function(e) {
+                this.style.opacity = '';
+            });
+            photoImg.addEventListener('mouseleave', function(e) {
+                this.style.opacity = '';
+            });
+            photoImg.addEventListener('touchstart', function(e) {
+                this.style.opacity = '0.8';
+            });
+            photoImg.addEventListener('touchend', function(e) {
+                this.style.opacity = '';
+            });
+            photoImg.addEventListener('touchcancel', function(e) {
+                this.style.opacity = '';
+            });
+            function setBtnActive() {
+                randomBtn.style.background = '#17242e';
+                randomBtn.style.transform = 'scale(0.98)';
+                randomBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
+            }
+            function setBtnNormal() {
+                randomBtn.style.background = '#1f2f3b';
+                randomBtn.style.transform = 'scale(1)';
+                randomBtn.style.boxShadow = 'none';
+            }
+            randomBtn.addEventListener('mousedown', setBtnActive);
+            randomBtn.addEventListener('mouseup', setBtnNormal);
+            randomBtn.addEventListener('mouseleave', setBtnNormal);
+            randomBtn.addEventListener('touchstart', setBtnActive);
+            randomBtn.addEventListener('touchend', setBtnNormal);
+            randomBtn.addEventListener('touchcancel', setBtnNormal);
+
+            photoImg.addEventListener('click', function() {
+                if (currentMusicLink) {
+                    window.open(currentMusicLink, '_blank');
+                }
+            });
+	    function randmusic() {
+	        let randid = lastIndex;
+	        while (randid === lastIndex) {
+	            randid = Math.floor(Math.random() * library.length);
+		}
+
+		if (lastIndex == -1) randid = 3;
+
+		lastIndex = randid;
+                const ys = library[randid];
+                photoImg.src = ys.cover;
+                photoImg.alt = ys.name;
+                nameEl.textContent = ys.name;
+                ambEl.textContent = ys.album;
+		writerEl.textContent = ys.writer;
+		recommenderEl.textContent = ys.user;
+                currentMusicLink = ys.click;
+            }
+
+            randmusic();
+
+            randomBtn.addEventListener('click', function() {
+                randmusic();
+            });
+
+        })();
+    </script>
+</div>
+
+
+
+		</div>
+	</div>
+</div>
+
+<!--
+<div class="row shadow my-4 wyoj-topcard" id="recommender-zone">
+	<div class="col-12 p-4">
+		<h3>服务器信息</h3>
+		<div id="submissions-data" data-submissions_data='<?= $submjson ?>'></div>
+		<div style="position: relative; width: 100%; padding-top: 20%;">
+			<div id="submissions-plot" class="w-100 h-100" style="position: absolute; top: 0; left: 0;"></div>
+		</div>
+		<div style="position: relative; width: 100%; padding-top: 20%;">
+			<div id="storage-plot" class="w-100 h-100" style="position: absolute; top: 0; left: 0;"></div>
+		</div>
+	</div>
+</div>
+-->
+
+<div class="row shadow my-4 wyoj-topcard">
+	<div class="col-12 p-4">
+		<h3>博客广场</h3>
+	</div>
+
+	<?php 
+		foreach ($topblogs as $id) echo echoBlogCard(queryBlog($id['id']));
+	?>
+</div>
+
+<div class="row shadow my-4 wyoj-topcard">
+	<div class="col-12 p-4">
+		<h3>比赛排行榜</h3>
+	</div>
+	<?php foreach($topusers as $user): ?>
+		<div class="col-md-3 mb-4">
+			<div class="card h-100 p-3 text-center">
+				<img class="uoj-avatar mx-auto mb-2" src=<?= "https://q.qlogo.cn/g?b=qq&nk=" . $user['qq'] . "&s=100" ?> alt="...">
+				<span class="mb-2"><?= getUserLink($user['username']) ?></span>
+				<span class="text-center text-bottom"><?= $user['motto'] ?></span>
+			</div>
+		</div>
+	<?php endforeach ?>
+</div>
+<script>
+(function() {
+    const submissions_data_raw = JSON.parse(document.getElementById('submissions-data').dataset.submissions_data);
+    const submissions_data = submissions_data_raw.map(x => [Number(new Date(x['weekst'])), Number(x['cnt'])]);
+    console.log(submissions_data);
+
+    var dataset = [
+        {
+            label: "提交数",
+            data: submissions_data,
+            yaxis: 1,
+            lines: { show: true },
+            points: { show: true }
+        },
+    ];
+
+    var options1 = {
+        yaxes: [
+            {
+                position: "left",
+                min: 0,
+                max: 10000,
+                axisLabelUseCanvas: true,
+                axisLabelFontSizePixels: 12
+            },
+            {
+                position: "right",
+                min: 0,
+                max: 10000,
+                tickDecimals: 0,
+                axisLabelUseCanvas: true,
+                axisLabelFontSizePixels: 12
+            }
+	],
+        xais: {
+            mode: 'time',
+            timeformat: "%Y-%m-%d",
+	    minTickSize: [1, 'day']
+        },
+        grid: {
+            hoverable: true,
+        },
+        legend: {
+            position: "ne"
+        }
+    };
+    $.plot($("#submissions-plot"), dataset, options1);
+}) ();
+</script>
+
+
+<?php echoUOJPageFooter() ?>

+ 43 - 0
app/controllers/judge/download.php

@@ -0,0 +1,43 @@
+<?php
+	requirePHPLib('judger');
+	
+	if (!authenticateJudger()) {
+		become404Page();
+	}
+	
+	switch ($_GET['type']) {
+		case 'submission':		
+			$file_name = UOJContext::storagePath()."/submission/{$_GET['id']}/{$_GET['rand_str_id']}";
+			$download_name = "submission.zip";
+			break;
+		case 'tmp':
+			$file_name = UOJContext::storagePath()."/tmp/{$_GET['rand_str_id']}";
+			$download_name = "tmp";
+			break;
+		case 'problem':
+			$id = $_GET['id'];
+			if (!validateUInt($id) || !($problem = queryProblemBrief($id))) {
+				become404Page();
+			}
+			$file_name = "/var/uoj_data/$id.zip";
+			$download_name = "$id.zip";
+			break;
+		case 'judger':
+			$file_name = UOJContext::storagePath()."/judge_client.zip";
+			$download_name = "judge_client.zip";
+			break;
+		default:
+			become404Page();
+	}
+	
+	$finfo = finfo_open(FILEINFO_MIME);
+	$mimetype = finfo_file($finfo, $file_name);
+	if ($mimetype === false) {
+		become404Page();
+	}
+	finfo_close($finfo);
+	
+	header("X-Sendfile: $file_name");
+	header("Content-type: $mimetype");
+	header("Content-Disposition: attachment; filename=$download_name");
+?>

+ 212 - 0
app/controllers/judge/submit.php

@@ -0,0 +1,212 @@
+<?php
+	requirePHPLib('judger');
+	requirePHPLib('data');
+	
+	if (!authenticateJudger()) {
+		become404Page();
+	}
+	
+	function submissionJudged() {
+		$submission = DB::selectFirst("select submitter, status, content, result, problem_id from submissions where id = {$_POST['id']}");
+		if ($submission == null) {
+			return;
+		}
+		if ($submission['status'] != 'Judging' && $submission['status'] != 'Judged, Judging') {
+			return;
+		}
+		$content = json_decode($submission['content'], true);
+		
+		if (isset($content['first_test_config'])) {
+			$result = json_decode($submission['result'], true);
+			$result['final_result'] = json_decode($_POST['result'], true);
+			$result['final_result']['details'] = uojTextEncode($result['final_result']['details']);
+			$esc_result = DB::escape(json_encode($result, JSON_UNESCAPED_UNICODE));
+			
+			$content['final_test_config'] = $content['config'];
+			$content['config'] = $content['first_test_config'];
+			unset($content['first_test_config']);
+			$esc_content = DB::escape(json_encode($content));
+			
+			DB::update("update submissions set status = 'Judged', result = '$esc_result', content = '$esc_content' where id = {$_POST['id']}");
+		} else {
+			$result = json_decode($_POST['result'], true);
+			$result['details'] = uojTextEncode($result['details']);
+			$esc_result = DB::escape(json_encode($result, JSON_UNESCAPED_UNICODE));
+			if (isset($result["error"])) {
+				DB::update("update submissions set status = '{$result['status']}', result_error = '{$result['error']}', result = '$esc_result', score = null, used_time = null, used_memory = null where id = {$_POST['id']}");
+			} else {
+				DB::update("update submissions set status = '{$result['status']}', result_error = null, result = '$esc_result', score = {$result['score']}, used_time = {$result['time']}, used_memory = {$result['memory']} where id = {$_POST['id']}");
+			}
+			
+			if (isset($content['final_test_config'])) {
+				$content['first_test_config'] = $content['config'];
+				$content['config'] = $content['final_test_config'];
+				unset($content['final_test_config']);
+				$esc_content = DB::escape(json_encode($content));
+			
+				DB::update("update submissions set status = 'Judged, Waiting', content = '$esc_content' where id = ${_POST['id']}");
+			}
+		}
+		DB::update("update submissions set status_details = '' where id = {$_POST['id']}");
+		updateBestACSubmissions($submission['submitter'], $submission['problem_id']);
+	}
+
+	function customTestSubmissionJudged() {
+		$submission = DB::selectFirst("select submitter, status, content, result, problem_id from custom_test_submissions where id = {$_POST['id']}");
+		if ($submission == null) {
+			return;
+		}
+		if ($submission['status'] != 'Judging') {
+			return;
+		}
+		$content = json_decode($submission['content'], true);
+		$result = json_decode($_POST['result'], true);
+		$result['details'] = uojTextEncode($result['details']);
+		$esc_result = DB::escape(json_encode($result, JSON_UNESCAPED_UNICODE));
+		if (isset($result["error"])) {
+			DB::update("update custom_test_submissions set status = '{$result['status']}', result = '$esc_result' where id = {$_POST['id']}");
+		} else {
+			DB::update("update custom_test_submissions set status = '{$result['status']}', result = '$esc_result' where id = {$_POST['id']}");
+		}
+		DB::update("update custom_test_submissions set status_details = '' where id = {$_POST['id']}");
+	}
+	
+	function hackJudged() {
+		$result = json_decode($_POST['result'], true);
+		$esc_details = DB::escape(uojTextEncode($result['details']));
+		$ok = DB::update("update hacks set success = {$result['score']}, details = '$esc_details' where id = {$_POST['id']}");
+		
+		if ($ok) {
+			list($hack_input) = DB::fetch(DB::query("select input from hacks where id = {$_POST['id']}"), MYSQLI_NUM);
+			unlink(UOJContext::storagePath().$hack_input);
+
+			if ($result['score']) {
+				list($problem_id) = DB::selectFirst("select problem_id from hacks where id = ${_POST['id']}", MYSQLI_NUM);
+				if (validateUploadedFile('hack_input') && validateUploadedFile('std_output')) {
+					dataAddExtraTest(queryProblemBrief($problem_id), $_FILES["hack_input"]["tmp_name"], $_FILES["std_output"]["tmp_name"]);
+				} else {
+					error_log("hack successfully but received no data. id: ${_POST['id']}");
+				}
+			}
+		}
+	}
+	
+	if (isset($_POST['submit'])) {
+		if (!validateUInt($_POST['id'])) {
+			die("Wow! hacker! T_T....");
+		}
+		if (isset($_POST['is_hack'])) {
+			hackJudged();
+		} elseif (isset($_POST['is_custom_test'])) {
+			customTestSubmissionJudged();
+		} else {
+			submissionJudged();
+		}
+	}
+	if (isset($_POST['update-status'])) {
+		if (!validateUInt($_POST['id'])) {
+			die("Wow! hacker! T_T....");
+		}
+		$esc_status_details = DB::escape($_POST['status']);
+		if (isset($_POST['is_custom_test'])) {
+			DB::update("update custom_test_submissions set status_details = '$esc_status_details' where id = {$_POST['id']}");
+		} else {
+			DB::update("update submissions set status_details = '$esc_status_details' where id = {$_POST['id']}");
+		}
+		die();
+	}
+	
+	$submission = null;
+	$hack = null;
+	function querySubmissionToJudge($status, $set_q) {
+		global $submission;
+		$submission = DB::selectFirst("select id, problem_id, content from submissions where status = '$status' order by id limit 1");
+		if ($submission) {
+			DB::update("update submissions set $set_q where id = {$submission['id']} and status = '$status'");
+			if (DB::affected_rows() != 1) {
+				$submission = null;
+			}
+		}
+	}
+	function queryCustomTestSubmissionToJudge() {
+		global $submission;
+		$submission = DB::selectFirst("select id, problem_id, content from custom_test_submissions where judge_time is null order by id limit 1");
+		if ($submission) {
+			DB::update("update custom_test_submissions set judge_time = now(), status = 'Judging' where id = {$submission['id']} and judge_time is null");
+			if (DB::affected_rows() != 1) {
+				$submission = null;
+			}
+		}
+		if ($submission) {
+			$submission['is_custom_test'] = '';
+		}
+	}
+	function queryHackToJudge() {
+		global $hack;
+		$hack = DB::selectFirst("select id, submission_id, input, input_type from hacks where judge_time is null order by id limit 1");
+		if ($hack) {
+			DB::update("update hacks set judge_time = now() where id = {$hack['id']} and judge_time is null");
+			if (DB::affected_rows() != 1) {
+				$hack = null;
+			}
+		}
+	}
+	function findSubmissionToJudge() {
+		global $submission, $hack;
+		querySubmissionToJudge('Waiting', "judge_time = now(), status = 'Judging'");
+		if ($submission) {
+			return true;
+		}
+
+		queryCustomTestSubmissionToJudge();
+		if ($submission) {
+			return true;
+		}
+		
+		querySubmissionToJudge('Waiting Rejudge', "judge_time = now(), status = 'Judging'");
+		if ($submission) {
+			return true;
+		}
+		
+		querySubmissionToJudge('Judged, Waiting', "status = 'Judged, Judging'");
+		if ($submission) {
+			return true;
+		}
+		
+		queryHackToJudge();
+		if ($hack) {
+			$submission = DB::selectFirst("select id, problem_id, content from submissions where id = {$hack['submission_id']} and score = 100");
+			if (!$submission) {
+				$details = "<error>the score gained by the hacked submission is not 100.\n</error>";
+				$esc_details = DB::escape(uojTextEncode($details));
+				DB::update("update hacks set success = 0, details = '$esc_details' where id = {$hack['id']}");
+				return false;
+			}
+			return true;
+		}
+		return false;
+	}
+	
+	
+	
+	if (isset($_POST['fetch_new']) && !$_POST['fetch_new']) {
+		die("Nothing to judge");
+	}
+	if (!findSubmissionToJudge()) {
+		die("Nothing to judge");
+	}
+	
+	$submission['id'] = (int)$submission['id'];
+	$submission['problem_id'] = (int)$submission['problem_id'];
+	$submission['problem_mtime'] = filemtime("/var/uoj_data/{$submission['problem_id']}");
+	$submission['content'] = json_decode($submission['content']);
+	
+	if ($hack) {
+		$submission['is_hack'] = "";
+		$submission['hack']['id'] = (int)$hack['id'];
+		$submission['hack']['input'] = $hack['input'];
+		$submission['hack']['input_type'] = $hack['input_type'];
+	}
+	
+	echo json_encode($submission);
+?>

+ 21 - 0
app/controllers/judge/sync_judge_client.php

@@ -0,0 +1,21 @@
+<?php
+	requirePHPLib('judger');
+	
+	if (!authenticateJudger()) {
+		become404Page();
+	}
+	
+	foreach (DB::selectAll("select * from judger_info where ip != ''") as $judger) {
+		$socket = fsockopen($judger['ip'], UOJConfig::$data['judger']['socket']['port']);
+		if ($socket === false) {
+			die("judge client {$judger['ip']} lost.");
+		}
+		fwrite($socket, json_encode([
+			'password' => UOJConfig::$data['judger']['socket']['password'],
+			'cmd' => 'update'
+		]));
+		fclose($socket);
+	}
+	
+	die("ok");
+?>

+ 120 - 0
app/controllers/login.php

@@ -0,0 +1,120 @@
+<?php
+	if (Auth::check()) {
+		redirectTo('/');
+	}
+	
+	function handleLoginPost() {
+		if (!crsf_check()) {
+			return 'expired';
+		}
+		if (!isset($_POST['username'])) {
+			return "failed";
+		}
+		if (!isset($_POST['password'])) {
+			return "failed";
+		}
+		$username = $_POST['username'];
+		$password = $_POST['password'];
+		
+		if (!validateUsername($username)) {
+			return "failed";
+		}
+		if (!validatePassword($password)) {
+			return "failed";
+		}
+		
+		$user = queryUser($username);
+		if (!$user || !checkPassword($user, $password)) {
+			return "failed";
+		}
+		
+		if ($user['usergroup'] == 'B') {
+			return "banned";
+		}
+		
+		Auth::login($user['username']);
+		return "ok";
+	}
+	
+	if (isset($_POST['login'])) {
+		echo handleLoginPost();
+		die();
+	}
+?>
+<?php
+	$REQUIRE_LIB['md5'] = '';
+?>
+<?php echoUOJPageHeader(UOJLocale::get('login')) ?>
+<h2 class="page-header"><?= UOJLocale::get('login') ?></h2>
+<form id="form-login" class="form-horizontal" method="post">
+  <div id="div-username" class="form-group">
+    <label for="input-username" class="col-sm-2 control-label"><?= UOJLocale::get('username') ?></label>
+    <div class="col-sm-3">
+      <input type="text" class="form-control" id="input-username" name="username" placeholder="<?= UOJLocale::get('enter your username') ?>" maxlength="20" />
+      <span class="help-block" id="help-username"></span>
+    </div>
+  </div>
+  <div id="div-password" class="form-group">
+    <label for="input-password" class="col-sm-2 control-label"><?= UOJLocale::get('password') ?></label>
+    <div class="col-sm-3">
+      <input type="password" class="form-control" id="input-password" name="password" placeholder="<?= UOJLocale::get('enter your password') ?>" 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"><?= UOJLocale::get('submit') ?></button>
+    </div>
+  </div>
+</form>
+
+<script type="text/javascript">
+function validateLoginPost() {
+	var ok = true;
+	ok &= getFormErrorAndShowHelp('username', validateUsername);
+	ok &= getFormErrorAndShowHelp('password', validatePassword);
+	return ok;
+}
+
+function submitLoginPost() {
+	if (!validateLoginPost()) {
+		return false;
+	}
+	
+	$.post('/login', {
+		_token : "<?= crsf_token() ?>",
+		login : '',
+		username : $('#input-username').val(),
+		password : md5($('#input-password').val(), "<?= getPasswordClientSalt() ?>")
+	}, function(msg) {
+		if (msg == 'ok') {
+			var prevUrl = document.referrer;
+			if (prevUrl == '' || /.*\/login.*/.test(prevUrl) || /.*\/logout.*/.test(prevUrl) || /.*\/register.*/.test(prevUrl) || /.*\/reset-password.*/.test(prevUrl)) {
+				prevUrl = '/';
+			};
+			window.location.href = prevUrl;
+		} else if (msg == 'banned') {
+			$('#div-username').addClass('has-error');
+			$('#help-username').html('该账号尚未激活或已被封禁。请依照<a href="https://oj.ryp.org.cn/blog/ryp/post/93">方法</a>进行激活');
+		} else if (msg == 'expired') {
+			$('#div-username').addClass('has-error');
+			$('#help-username').html('页面会话已过期。');
+		} else {
+			$('#div-username').addClass('has-error');
+			$('#help-username').html('用户名或密码错误。');
+			$('#div-password').addClass('has-error');
+			$('#help-password').html('用户名或密码错误。<a href="/forgot-password">忘记密码?</a>');
+		}
+	});
+	return true;
+}
+
+$(document).ready(function() {
+	$('#form-login').submit(function(e) {
+		e.preventDefault();
+		submitLoginPost();
+	});
+});
+
+</script>
+<?php echoUOJPageFooter() ?>

+ 12 - 0
app/controllers/logout.php

@@ -0,0 +1,12 @@
+<?php
+	crsf_defend();
+	Auth::logout();
+?>
+
+<script type="text/javascript">
+var prevUrl = document.referrer;
+if (!prevUrl) {
+  prevUrl = '/';
+};
+window.location.href = prevUrl;
+</script>

+ 132 - 0
app/controllers/map_visualizer.php

@@ -0,0 +1,132 @@
+<?php
+
+$REQUIRE_LIB['shjs'] = "";
+$REQUIRE_LIB['dracula'] = "";
+$REQUIRE_LIB['base64'] = "";
+$REQUIRE_LIB['raphael'] = "";
+echoUOJPageHeader("图可视化");
+
+?>
+<div class="container">
+	<div class="row">
+		<div class="col-md-3">
+			<div class="form-check">
+				<input class="form-check-input" type="checkbox" id="directed" name="directed">
+				<label class="form-check-label" for="directed">
+					是否有向
+				</label>
+			</div>
+			<div class="form-group">
+				<textarea class="form-control" id="edges" rows="3" placeholder="格式:
+第一条边起点 第一条边终点 [第一条边权重]
+第二条边起点 第二条边终点 [第二条边权重]
+第三条边起点 第三条边终点 [第三条边权重]
+如:
+1 2
+2 3
+1 3 2
+"></textarea>
+			</div>
+			<?php if (UOJConfig::$data['tools']['map-copy-enabled']): ?>
+			<button type="button" class="btn btn-secondary" id="copy">复制图片</button>
+			<?php endif ?>
+		</div>
+		<div class="col-md-9" id="paper">
+		</div>
+	</div>
+</div>
+<script>
+	(function () {
+		<?php if (UOJConfig::$data['tools']['map-copy-enabled']): ?>
+		$("#copy").click(function () {
+			if(navigator.permissions) {
+				navigator.permissions.query({name: "clipboard-write"}).then(result => {
+					if (result.state === "granted" || result.state === "prompt") {
+						let paper = $("#paper")
+						// First, we create an canvas and a img which has the same size with the canvas.
+						let canvas = document.createElement('canvas');
+						let s_img = document.createElement('img');
+						s_img.height = paper.height()
+						s_img.width = paper.width()
+						canvas.height = paper.height()
+						canvas.width = paper.width()
+						// After this image is loaded, we draw it on our canvas
+						s_img.onload = function () {
+							// On some browsers, the exported png have a black background.
+							// So we will draw a white background first.
+							ctx = canvas.getContext('2d');
+							ctx.beginPath();
+							ctx.rect(0, 0, paper.width(), paper.height());
+							ctx.fillStyle = "white";
+							ctx.fill();
+							// We put our image created from the svg to the canvas
+							canvas.getContext('2d').drawImage(s_img, 0, 0);
+							// Then we export canvas as a png file, paste it to clipboard.
+							canvas.toBlob(function (blob) {
+								const item = new ClipboardItem({[blob.type]: blob});
+								navigator.clipboard.write([item]).then(function () {
+									alert("图片已经复制到剪切板中");
+								}, function (err) {
+									alert(err);
+								});
+							});
+						}
+						// Load our svg to this image.
+						s_img.src = "data:image/svg+xml;base64," + Base64.encode(new XMLSerializer().serializeToString(document.querySelector("#paper > svg")));
+					} else {
+						alert("获取剪切板权限失败, 请打开网页设置并授予剪切板权限!")
+					}
+				});
+			} else {
+				alert("获取剪切板权限失败, 请使用最新Chrome浏览器, 打开网页设置并授予剪切板权限!")
+			}
+		})
+		<?php endif ?>
+		let repaint = function () {
+			let paper = $("#paper");
+			// Clear existing elements.
+			paper.html("");
+			let directed = $('#directed').is(":checked");
+			let Graph = Dracula.Graph;
+			let Renderer = Dracula.Renderer.Raphael;
+			let Layout = Dracula.Layout.Spring;
+			let graph = new Graph();
+			let render = function(r, n) {
+				let color = Raphael.getColor();
+				return r.set()
+					.push(
+						r.ellipse(0, 0, 30, 20).attr({stroke: color, "stroke-width": 2, fill: color, "fill-opacity": 0})
+					)
+					.push(r.text(0, 0, n.id).attr({ opacity: 1, 'font-size': 20,}));
+			}
+			$("#edges").val().split("\n").forEach(function (edge) {
+				if (edge.split(" ").length >= 2) {
+					if (edge.split(" ")[0].length !== 0 && edge.split(" ")[1].length !== 0 ){
+						graph.addNode(edge.split(" ")[0],  {
+							render: render
+						});
+						graph.addNode(edge.split(" ")[1],  {
+							render: render
+						});
+						graph.addEdge(edge.split(" ")[0], edge.split(" ")[1], {
+							directed: directed,
+							label:edge.split(" ")[2],
+							'label-style' : {
+								'font-size': 20,
+								"fill-opacity":"1"
+							}
+						});
+					}
+				}
+			})
+			var layout = new Layout(graph)
+			var renderer = new Renderer('#paper', graph, paper.width(), paper.height())
+			renderer.draw()
+		}
+		$("#edges").on("input", repaint)
+		$("#directed").on("input", repaint)
+	})()
+</script>
+
+<?php
+echoUOJPageFooter();

+ 37 - 0
app/controllers/paste_post.php

@@ -0,0 +1,37 @@
+<?php
+requirePHPLib('form');
+requirePHPLib('judger');
+
+if ($myUser == null) {
+	becomeMsgPage("请先登录!");
+}
+
+
+function handleUpload($zip_file_name, $content, $tot_size) {
+	global $myUser;
+	$esc_content = DB::escape(json_encode($content));
+	$index = uojRandString(20);
+	$esc_index = DB::escape($index);
+	while (DB::selectFirst("select count(*) as count from pastes where `index` = '$esc_index'")['count'] != "0") {
+		$index = uojRandString(20);
+		$esc_index = DB::escape($index);
+	}
+	DB::query("insert into pastes (`index`, `creator`, `content`, `created_at`) values ('$esc_index', '${myUser['username']}', '$esc_content', '".date('Y-m-d H:i:s')."')");
+	redirectTo("/pastes/".$index);
+}
+
+$paste_form = newSubmissionForm('paste',
+	[
+		[
+			'type' => "source code",
+			"name" => "paste",
+			"file_name" => "paste.code"
+		]
+	],
+	'uojRandAvaiablePasteFileName',
+	'handleUpload');
+$paste_form->succ_href = '/paste';
+$paste_form->runAtServer();
+echoUOJPageHeader("Paste!");
+$paste_form->printHTML();
+echoUOJPageFooter();

+ 12 - 0
app/controllers/paste_view.php

@@ -0,0 +1,12 @@
+<?php
+
+$paste_id = $_GET['rand_str_id'];
+
+$paste = DB::selectFirst("select * from pastes where `index` = '".DB::escape($paste_id)."'");
+if (!$paste) {
+	become404Page();
+}
+$REQUIRE_LIB['shjs'] = "";
+echoUOJPageHeader("Paste!");
+echoPasteContent($paste);
+echoUOJPageFooter();

+ 276 - 0
app/controllers/problem.php

@@ -0,0 +1,276 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	
+	if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
+		become404Page();
+	}
+	
+	$problem_content = queryProblemContent($problem['id']);
+	
+	$contest = validateUInt($_GET['contest_id']) ? queryContest($_GET['contest_id']) : null;
+	if ($contest != null) {
+		genMoreContestInfo($contest);
+		$problem_rank = queryContestProblemRank($contest, $problem);
+		if ($problem_rank == null) {
+			become404Page();
+		} else {
+			$problem_letter = chr(ord('A') + $problem_rank - 1);
+		}
+	}
+	
+	$is_in_contest = false;
+	$ban_in_contest = false;
+	if ($contest != null) {
+		if (!hasContestPermission($myUser, $contest)) {
+			if ($contest['cur_progress'] == CONTEST_NOT_STARTED) {
+				become404Page();
+			} elseif ($contest['cur_progress'] == CONTEST_IN_PROGRESS) {
+				if ($myUser == null || !hasRegistered($myUser, $contest)) {
+					becomeMsgPage("<h1>比赛正在进行中</h1><p>很遗憾,您尚未报名。比赛结束后再来看吧~</p>");
+				} else {
+					$is_in_contest = true;
+					DB::update("update contests_registrants set has_participated = 1 where username = '{$myUser['username']}' and contest_id = {$contest['id']}");
+				}
+			} else {
+				$ban_in_contest = !isProblemVisibleToUser($problem, $myUser);
+			}
+		}
+	} else {
+		if (!isProblemVisibleToUser($problem, $myUser)) {
+			become404Page();
+		}
+	}
+
+	$submission_requirement = json_decode($problem['submission_requirement'], true);
+	$problem_extra_config = getProblemExtraConfig($problem);
+	$custom_test_requirement = getProblemCustomTestRequirement($problem);
+
+	if ($custom_test_requirement && Auth::check()) {
+		$custom_test_submission = DB::selectFirst("select * from custom_test_submissions where submitter = '".Auth::id()."' and problem_id = {$problem['id']} order by id desc limit 1");
+		$custom_test_submission_result = json_decode($custom_test_submission['result'], true);
+	}
+	if ($custom_test_requirement && $_GET['get'] == 'custom-test-status-details' && Auth::check()) {
+		if ($custom_test_submission == null) {
+			echo json_encode(null);
+		} elseif ($custom_test_submission['status'] != 'Judged') {
+			echo json_encode(array(
+				'judged' => false,
+				'html' => getSubmissionStatusDetails($custom_test_submission)
+			));
+		} else {
+			ob_start();
+			$styler = new CustomTestSubmissionDetailsStyler();
+			if (!hasViewPermission($problem_extra_config['view_details_type'], $myUser, $problem, $submission)) {
+				$styler->fade_all_details = true;
+			}
+			echoJudgementDetails($custom_test_submission_result['details'], $styler, 'custom_test_details');
+			$result = ob_get_contents();
+			ob_end_clean();
+			echo json_encode(array(
+				'judged' => true,
+				'html' => getSubmissionStatusDetails($custom_test_submission),
+				'result' => $result
+			));
+		}
+		die();
+	}
+	
+	$can_use_zip_upload = true;
+	foreach ($submission_requirement as $req) {
+		if ($req['type'] == 'source code') {
+			$can_use_zip_upload = false;
+		}
+	}
+	
+	function handleUpload($zip_file_name, $content, $tot_size) {
+		global $problem, $contest, $myUser, $is_in_contest;
+		
+		$content['config'][] = array('problem_id', $problem['id']);
+		if ($is_in_contest && $contest['extra_config']["contest_type"]!='IOI' && !isset($contest['extra_config']["problem_{$problem['id']}"])) {
+			$content['final_test_config'] = $content['config'];
+			$content['config'][] = array('test_sample_only', 'on');
+		}
+		$esc_content = DB::escape(json_encode($content));
+
+		$language = '/';
+		foreach ($content['config'] as $row) {
+			if (strEndWith($row[0], '_language')) {
+				$language = $row[1];
+				break;
+			}
+		}
+		if ($language != '/') {
+			Cookie::set('uoj_preferred_language', $language, time() + 60 * 60 * 24 * 365, '/');
+		}
+		$esc_language = DB::escape($language);
+ 		
+		$result = array();
+		$result['status'] = "Waiting";
+		$result_json = json_encode($result);
+		
+		if ($is_in_contest) {
+			DB::query("insert into submissions (problem_id, contest_id, submit_time, submitter, content, language, tot_size, status, result, is_hidden) values (${problem['id']}, ${contest['id']}, now(), '${myUser['username']}', '$esc_content', '$esc_language', $tot_size, '${result['status']}', '$result_json', 0)");
+		} else {
+			DB::query("insert into submissions (problem_id, submit_time, submitter, content, language, tot_size, status, result, is_hidden) values (${problem['id']}, now(), '${myUser['username']}', '$esc_content', '$esc_language', $tot_size, '${result['status']}', '$result_json', {$problem['is_hidden']})");
+		}
+	}
+	function handleCustomTestUpload($zip_file_name, $content, $tot_size) {
+		global $problem, $contest, $myUser;
+		
+		$content['config'][] = array('problem_id', $problem['id']);
+		$content['config'][] = array('custom_test', 'on');
+		$esc_content = DB::escape(json_encode($content));
+
+		$language = '/';
+		foreach ($content['config'] as $row) {
+			if (strEndWith($row[0], '_language')) {
+				$language = $row[1];
+				break;
+			}
+		}
+		if ($language != '/') {
+			Cookie::set('uoj_preferred_language', $language, time() + 60 * 60 * 24 * 365, '/');
+		}
+		$esc_language = DB::escape($language);
+ 		
+		$result = array();
+		$result['status'] = "Waiting";
+		$result_json = json_encode($result);
+		
+		DB::insert("insert into custom_test_submissions (problem_id, submit_time, submitter, content, status, result) values ({$problem['id']}, now(), '{$myUser['username']}', '$esc_content', '{$result['status']}', '$result_json')");
+	}
+	
+	if ($can_use_zip_upload) {
+		$zip_answer_form = newZipSubmissionForm('zip_answer',
+			$submission_requirement,
+			'uojRandAvaiableSubmissionFileName',
+			'handleUpload');
+		$zip_answer_form->extra_validator = function() {
+			global $ban_in_contest;
+			if ($ban_in_contest) {
+				return '请耐心等待比赛结束后题目对所有人可见了再提交';
+			}
+			return '';
+		};
+		$zip_answer_form->succ_href = $is_in_contest ? "/contest/{$contest['id']}/submissions" : '/submissions';
+		$zip_answer_form->runAtServer();
+	}
+	
+	$answer_form = newSubmissionForm('answer',
+		$submission_requirement,
+		'uojRandAvaiableSubmissionFileName',
+		'handleUpload');
+	$answer_form->extra_validator = function() {
+		global $ban_in_contest;
+		if ($ban_in_contest) {
+			return '请耐心等待比赛结束后题目对所有人可见了再提交';
+		}
+		return '';
+	};
+	$answer_form->succ_href = $is_in_contest ? "/contest/{$contest['id']}/submissions" : '/submissions';
+	$answer_form->runAtServer();
+
+	if ($custom_test_requirement) {
+		$custom_test_form = newSubmissionForm('custom_test',
+			$custom_test_requirement,
+			function() {
+				return uojRandAvaiableFileName('/tmp/');
+			},
+			'handleCustomTestUpload');
+		$custom_test_form->appendHTML(<<<EOD
+<div id="div-custom_test_result"></div>
+EOD
+		);
+		$custom_test_form->succ_href = 'none';
+		$custom_test_form->extra_validator = function() {
+			global $ban_in_contest, $custom_test_submission;
+			if ($ban_in_contest) {
+				return '请耐心等待比赛结束后题目对所有人可见了再提交';
+			}
+			if ($custom_test_submission && $custom_test_submission['status'] != 'Judged') {
+				return '上一个测评尚未结束';
+			}
+			return '';
+		};
+		$custom_test_form->ctrl_enter_submit = true;
+		$custom_test_form->setAjaxSubmit(<<<EOD
+function(response_text) {custom_test_onsubmit(response_text, $('#div-custom_test_result')[0], '{$_SERVER['REQUEST_URI']}?get=custom-test-status-details')}
+EOD
+		);
+		$custom_test_form->submit_button_config['text'] = UOJLocale::get('problems::run');
+		$custom_test_form->runAtServer();
+	}
+?>
+<?php
+	$REQUIRE_LIB['mathjax'] = '';
+	$REQUIRE_LIB['hljs'] = '';
+?>
+<?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - ' . UOJLocale::get('problems::problem')) ?>
+<?php
+	$limit = getUOJConf("/var/uoj_data/{$problem['id']}/problem.conf");
+	$time_limit = $limit['time_limit'];
+	$memory_limit = $limit['memory_limit'];
+?>
+<div class="row d-flex justify-content-center">
+	<span class="badge badge-secondary mr-1">时间限制:<?=$time_limit!=null?"$time_limit s":"N/A"?></span>
+	<span class="badge badge-secondary mr-1">空间限制:<?=$memory_limit!=null?"$memory_limit MB":"N/A"?></span>
+	<span class="badge badge-secondary mr-1">控制组: <?php echo $problem['ctrl'] == '' ? '默认' : $problem['ctrl'] ?></span>
+	<span class="badge badge-secondary mr-1">压缩包大小: <?php echo printSize(queryProblemSize($problem['id'])) ?></span>
+</div>
+<div class="float-right">
+	<?= getClickZanBlock('P', $problem['id'], $problem['zan']) ?>
+</div>
+
+<?php if ($contest): ?>
+<div class="page-header row">
+	<h1 class="col-md-3 text-left"><small><?= $contest['name'] ?></small></h1>
+	<h1 class="col-md-7 text-center"><?= $problem_letter ?>. <?= $problem['title'] ?></h1>
+	<div class="col-md-2 text-right" id="contest-countdown"></div>
+</div>
+<a role="button" class="btn btn-info float-right" href="/contest/<?= $contest['id'] ?>/problem/<?= $problem['id'] ?>/statistics"><span class="glyphicon glyphicon-stats"></span> <?= UOJLocale::get('problems::statistics') ?></a>
+<?php if ($contest['cur_progress'] <= CONTEST_IN_PROGRESS): ?>
+<script type="text/javascript">
+checkContestNotice(<?= $contest['id'] ?>, '<?= UOJTime::$time_now_str ?>');
+$('#contest-countdown').countdown(<?= $contest['end_time']->getTimestamp() - UOJTime::$time_now->getTimestamp() ?>);
+</script>
+<?php endif ?>
+<?php else: ?>
+<h1 class="page-header text-center">#<?= $problem['id']?>. <?= $problem['title'] ?></h1>
+<a role="button" class="btn btn-info float-right" href="/problem/<?= $problem['id'] ?>/statistics"><span class="glyphicon glyphicon-stats"></span> <?= UOJLocale::get('problems::statistics') ?></a>
+<?php endif ?>
+
+<ul class="nav nav-tabs" role="tablist">
+	<li class="nav-item"><a class="nav-link active" href="#tab-statement" role="tab" data-toggle="tab"><span class="glyphicon glyphicon-book"></span> <?= UOJLocale::get('problems::statement') ?></a></li>
+	<li class="nav-item"><a class="nav-link" href="#tab-submit-answer" role="tab" data-toggle="tab"><span class="glyphicon glyphicon-upload"></span> <?= UOJLocale::get('problems::submit') ?></a></li>
+	<?php if ($custom_test_requirement): ?>
+	<li class="nav-item"><a class="nav-link" href="#tab-custom-test" role="tab" data-toggle="tab"><span class="glyphicon glyphicon-console"></span> <?= UOJLocale::get('problems::custom test') ?></a></li>
+	<?php endif ?>
+	<?php if (hasProblemCtrlPermission($myUser, $problem)): ?>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab"><?= UOJLocale::get('problems::manage') ?></a></li>
+	<?php endif ?>
+	<?php if ($contest): ?>
+	<li class="nav-item"><a class="nav-link" href="/contest/<?= $contest['id'] ?>" role="tab"><?= UOJLocale::get('contests::back to the contest') ?></a></li>
+	<?php endif ?>
+</ul>
+<div class="tab-content">
+	<div class="tab-pane active" id="tab-statement">
+		<article class="top-buffer-md"><?= $problem_content['statement'] ?></article>
+	</div>
+	<div class="tab-pane" id="tab-submit-answer">
+		<div class="top-buffer-sm"></div>
+		<?php if ($can_use_zip_upload): ?>
+		<?php $zip_answer_form->printHTML(); ?>
+		<hr />
+		<strong><?= UOJLocale::get('problems::or upload files one by one') ?><br /></strong>
+		<?php endif ?>
+		<?php $answer_form->printHTML(); ?>
+	</div>
+	<?php if ($custom_test_requirement): ?>
+	<div class="tab-pane" id="tab-custom-test">
+		<div class="top-buffer-sm"></div>
+		<?php $custom_test_form->printHTML(); ?>
+	</div>
+	<?php endif ?>
+</div>
+<?php echoUOJPageFooter() ?>

+ 821 - 0
app/controllers/problem_data_manage.php

@@ -0,0 +1,821 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	requirePHPLib('data');
+	
+	if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
+		become404Page();
+	}
+	if (!hasProblemCtrlPermission($myUser, $problem)) {
+		become403Page();
+	}
+	$group = queryUser($problem['ctrl']);
+	$oj_name = UOJConfig::$data['profile']['oj-name'];
+	$problem_extra_config = getProblemExtraConfig($problem);
+
+	$data_dir = "/var/uoj_data/${problem['id']}";
+
+	function echoFileNotFound($file_name) {
+		echo '<h4>', htmlspecialchars($file_name), '<sub class="text-danger"> ', '文件未找到', '</sub></h4>';
+	}
+	function echoFilePre($file_name) {
+		global $data_dir;
+		$file_full_name = $data_dir . '/' . $file_name;
+
+		$finfo = finfo_open(FILEINFO_MIME);
+		$mimetype = finfo_file($finfo, $file_full_name);
+		if ($mimetype === false) {
+			echoFileNotFound($file_name);
+			return;
+		}
+		finfo_close($finfo);
+
+		echo '<h4>', htmlspecialchars($file_name), '<sub> ', $mimetype, '</sub></h4>';
+		echo "<pre>\n";
+
+		$output_limit = 1000;
+		if (strStartWith($mimetype, 'text/')) {
+			echo htmlspecialchars(uojFilePreview($file_full_name, $output_limit));
+		} else {
+			echo htmlspecialchars(uojFilePreview($file_full_name, $output_limit, 'binary'));
+		}
+		echo "\n</pre>";
+	}
+
+
+	//上传数据
+	if ($_POST['problem_data_file_submit']=='submit') {
+		if ($_FILES["problem_data_file"]["error"] > 0) {
+			$errmsg = "Error: ".$_FILES["problem_data_file"]["error"];
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		} else {
+			$zip_mime_types = array('application/zip', 'application/x-zip', 'application/x-zip-compressed');
+			if (in_array($_FILES["problem_data_file"]["type"], $zip_mime_types) || $_FILES["problem_data_file"]["type"] == 'application/octet-stream' && substr($_FILES["problem_data_file"]["name"], -4) == '.zip') {
+				$up_filename="/tmp/".rand(0,100000000)."data.zip";
+				move_uploaded_file($_FILES["problem_data_file"]["tmp_name"], $up_filename);
+				if (queryGroupTotalSize($group) + filesize($up_filename) > queryGroupQuota($group)) {
+					$errmsg = '组空间配额超限。';
+					unlink($up_filename);
+					becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+				}
+
+				$zip = new ZipArchive;
+				if ($zip->open($up_filename) === TRUE) {
+					$zip->extractTo("/var/uoj_data/upload/{$problem['id']}");
+					$zip->close();
+					exec("cd /var/uoj_data/upload/{$problem['id']}; if [ -z \"`find . -maxdepth 1 -type f`\" ]; then for sub_dir in `find -maxdepth 1 -type d ! -name .`; do mv -f \$sub_dir/* . && rm -rf \$sub_dir; done; fi");
+					echo "<script>alert('上传成功!')</script>";
+				} else {
+					$errmsg = "解压失败!";
+					becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+				}
+				unlink($up_filename);
+			} else {
+				$errmsg = "请上传zip格式!";
+				becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+			}
+		}
+	}
+
+	//添加配置文件
+	if ($_POST['problem_settings_file_submit']=='submit') {
+		if ($_POST['use_builtin_checker'] and $_POST['n_tests'] and $_POST['input_pre'] and $_POST['input_suf'] and $_POST['output_pre'] and $_POST['output_suf'] and $_POST['time_limit'] and $_POST['memory_limit']) {
+			$set_filename="/var/uoj_data/upload/{$problem['id']}/problem.conf";
+			$has_legacy=false;
+			if (file_exists($set_filename)) {
+				$has_legacy=true;
+				unlink($set_filename);
+			}
+			$setfile = fopen($set_filename, "w");
+			fwrite($setfile, "use_builtin_judger on\n");
+			if ($_POST['use_builtin_checker'] != 'ownchk') {
+				fwrite($setfile, "use_builtin_checker ".$_POST['use_builtin_checker']."\n");
+			}
+			fwrite($setfile, "n_tests ".$_POST['n_tests']."\n");
+			if ($_POST['n_ex_tests']) {
+				fwrite($setfile, "n_ex_tests ".$_POST['n_ex_tests']."\n");
+			} else {
+				fwrite($setfile, "n_ex_tests 0\n");
+			}
+			if ($_POST['n_sample_tests']) {
+				fwrite($setfile, "n_sample_tests ".$_POST['n_sample_tests']."\n");
+			} else {
+				fwrite($setfile, "n_sample_tests 0\n");
+			}
+			fwrite($setfile, "input_pre ".$_POST['input_pre']."\n");
+			fwrite($setfile, "input_suf ".$_POST['input_suf']."\n");
+			fwrite($setfile, "output_pre ".$_POST['output_pre']."\n");
+			fwrite($setfile, "output_suf ".$_POST['output_suf']."\n");
+			fwrite($setfile, "time_limit ".$_POST['time_limit']."\n");
+			fwrite($setfile, "memory_limit ".$_POST['memory_limit']."\n");
+			fclose($setfile);
+			if (!$has_legacy) {
+				echo "<script>alert('添加成功!')</script>";
+			} else {
+				echo "<script>alert('替换成功!')</script>";
+			}
+		} else {
+			$errmsg = "添加配置文件失败,请检查是否所有输入框都已填写!";
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		}
+	}
+
+
+	$info_form = new UOJForm('info');
+	$http_host = HTML::escape(UOJContext::httpHost());
+	$download_url = HTML::url("/download.php?type=problem&id={$problem['id']}");
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<!--<label class="col-sm-3 control-label">zip上传数据</label>
+	<div class="col-sm-9">
+		<div class="form-control-static">
+			<row>
+			<button type="button" style="width:30%" class="btn btn-primary" data-toggle="modal" data-target="#UploadDataModal">上传数据</button>
+			<button type="submit" style="width:30%" id="button-submit-data" name="submit-data" value="data" class="btn btn-danger">检验配置并同步数据</button>
+			</row>
+		</div>
+	</div>-->
+</div>
+EOD
+	);
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">problem_{$problem['id']}.zip</label>
+	<div class="col-sm-9">
+		<div class="form-control-static">
+			<a href="$download_url">$download_url</a>
+		</div>
+	</div>
+</div>
+EOD
+	);
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">testlib.h</label>
+	<div class="col-sm-9">
+		<div class="form-control-static">
+			<a href="/download.php?type=testlib.h">下载</a>
+		</div>
+	</div>
+</div>
+EOD
+	);
+
+	$esc_submission_requirement = HTML::escape(json_encode(json_decode($problem['submission_requirement']), JSON_PRETTY_PRINT));
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">提交文件配置</label>
+	<div class="col-sm-9">
+		<div class="form-control-static"><pre>
+$esc_submission_requirement
+</pre>
+		</div>
+	</div>
+</div>
+EOD
+	);
+	$esc_extra_config = HTML::escape(json_encode(json_decode($problem['extra_config']), JSON_PRETTY_PRINT));
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">其它配置</label>
+	<div class="col-sm-9">
+		<div class="form-control-static"><pre>
+$esc_extra_config
+</pre>
+		</div>
+	</div>
+</div>
+EOD
+	);
+	if (isSuperUser($myUser)) {
+		$info_form->addVInput('submission_requirement', 'text', '提交文件配置', $problem['submission_requirement'],
+			function ($submission_requirement, &$vdata) {
+				$submission_requirement = json_decode($submission_requirement, true);
+				if ($submission_requirement === null) {
+					return '不是合法的JSON';
+				}
+				$vdata['submission_requirement'] = json_encode($submission_requirement);
+			},
+			null);
+		$info_form->addVInput('extra_config', 'text', '其它配置', $problem['extra_config'],
+			function ($extra_config, &$vdata) {
+				$extra_config = json_decode($extra_config, true);
+				if ($extra_config === null) {
+					return '不是合法的JSON';
+				}
+				$vdata['extra_config'] = json_encode($extra_config);
+			},
+			null);
+		$info_form->handle = function(&$vdata) {
+			global $problem;
+			$esc_submission_requirement = DB::escape($vdata['submission_requirement']);
+			$esc_extra_config = DB::escape($vdata['extra_config']);
+			DB::update("update problems set submission_requirement = '$esc_submission_requirement', extra_config = '$esc_extra_config' where id = {$problem['id']}");
+		};
+	} else {
+		$info_form->no_submit = true;
+	}
+
+	class DataDisplayer {
+		public $problem_conf = array();
+		public $data_files = array();
+		public $displayers = array();
+
+		public function __construct($problem_conf = null, $data_files = null) {
+			global $data_dir;
+
+			if (isset($problem_conf)) {
+				foreach ($problem_conf as $key => $val) {
+					$this->problem_conf[$key] = array('val' => $val);
+				}
+			}
+
+			if (!isset($data_files)) {
+				$this->data_files = array_filter(scandir($data_dir), function($x) {
+					return $x !== '.' && $x !== '..' && $x !== 'problem.conf';
+				});
+				natsort($this->data_files);
+				array_unshift($this->data_files, 'problem.conf');
+			} else {
+				$this->data_files = $data_files;
+			}
+
+			$this->setDisplayer('problem.conf', function($self) {
+				global $info_form;
+				$info_form->printHTML();
+				echo '<div class="top-buffer-md"></div>';
+
+				echo '<table class="table table-bordered table-hover table-striped table-text-center">';
+				echo '<thead>';
+				echo '<tr>';
+				echo '<th>key</th>';
+				echo '<th>value</th>';
+				echo '</tr>';
+				echo '</thead>';
+				echo '<tbody>';
+				foreach ($self->problem_conf as $key => $info) {
+					if (!isset($info['status'])) {
+						echo '<tr>';
+						echo '<td>', htmlspecialchars($key), '</td>';
+						echo '<td>', htmlspecialchars($info['val']), '</td>';
+						echo '</tr>';
+					} elseif ($info['status'] == 'danger') {
+						echo '<tr class="text-danger">';
+						echo '<td>', htmlspecialchars($key), '</td>';
+						echo '<td>', htmlspecialchars($info['val']), ' <span class="glyphicon glyphicon-remove"></span>', '</td>';
+						echo '</tr>';
+					}
+				}
+				echo '</tbody>';
+				echo '</table>';
+
+				echoFilePre('problem.conf');
+			});
+		}
+
+		public function setProblemConfRowStatus($key, $status) {
+			$this->problem_conf[$key]['status'] = $status;
+			return $this;
+		}
+
+		public function setDisplayer($file_name, $fun) {
+			$this->displayers[$file_name] = $fun;
+			return $this;
+		}
+		public function addDisplayer($file_name, $fun) {
+			$this->data_files[] = $file_name;
+			$this->displayers[$file_name] = $fun;
+			return $this;
+		}
+		public function echoDataFilesList($active_file) {
+			foreach ($this->data_files as $file_name) {
+				echo '<li class="nav-item">';
+				if ($file_name != $active_file) {
+					echo '<a class="nav-link" href="#">';
+				} else {
+					echo '<a class="nav-link active" href="#">';
+				}
+				echo htmlspecialchars($file_name), '</a>', '</li>';
+			}
+		}
+		public function displayFile($file_name) {
+			global $data_dir;
+
+			if (isset($this->displayers[$file_name])) {
+				$fun = $this->displayers[$file_name];
+				$fun($this);
+			} elseif (in_array($file_name, $this->data_files)) {
+				echoFilePre($file_name);
+			} else {
+				echoFileNotFound($file_name);
+			}
+		}
+	}
+
+	function getDataDisplayer() {
+		global $data_dir;
+		global $problem;
+
+		$allow_files = array_flip(array_filter(scandir($data_dir), function($x) {
+			return $x !== '.' && $x !== '..';
+		}));
+
+		$getDisplaySrcFunc = function($name) use ($allow_files) {
+			return function() use ($name, $allow_files) {
+				$src_name = $name . '.cpp';
+				if (isset($allow_files[$src_name])) {
+					echoFilePre($src_name);
+				} else {
+					echoFileNotFound($src_name);
+				}
+				if (isset($allow_files[$name])) {
+					echoFilePre($name);
+				} else {
+					echoFileNotFound($name);
+				}
+			};
+		};
+
+		$problem_conf = getUOJConf("$data_dir/problem.conf");
+		if ($problem_conf === -1) {
+			return (new DataDisplayer())->setDisplayer('problem.conf', function() {
+				global $info_form;
+				$info_form->printHTML();
+				echoFileNotFound('problem.conf');
+			});
+		}
+		if ($problem_conf === -2) {
+			return (new DataDisplayer())->setDisplayer('problem.conf', function() {
+				global $info_form;
+				$info_form->printHTML();
+				echo '<h4 class="text-danger">problem.conf 格式有误</h4>';
+				echoFilePre('problem.conf');
+			});
+		}
+
+		$judger_name = getUOJConfVal($problem_conf, 'use_builtin_judger', null);
+		if (!isset($problem_conf['use_builtin_judger'])) {
+			return new DataDisplayer($problem_conf);
+		}
+		if ($problem_conf['use_builtin_judger'] == 'on') {
+			$n_tests = getUOJConfVal($problem_conf, 'n_tests', 10);
+			if (!validateUInt($n_tests)) {
+				return (new DataDisplayer($problem_conf))->setProblemConfRowStatus('n_tests', 'danger');
+			}
+
+			$has_extra_tests = !(isset($problem_conf['submit_answer']) && $problem_conf['submit_answer'] == 'on');
+
+			$data_disp = new DataDisplayer($problem_conf, array('problem.conf'));
+			$data_disp->addDisplayer('tests',
+				function($self) use ($problem_conf, $allow_files, $n_tests, $n_ex_tests) {
+					for ($num = 1; $num <= $n_tests; $num++) {
+						$input_file_name = getUOJProblemInputFileName($problem_conf, $num);
+						$output_file_name = getUOJProblemOutputFileName($problem_conf, $num);
+						echo '<div class="row">';
+						echo '<div class="col-md-6">';
+						if (isset($allow_files[$input_file_name])) {
+							echoFilePre($input_file_name);
+						} else {
+							echoFileNotFound($input_file_name);
+						}
+						echo '</div>';
+						echo '<div class="col-md-6">';
+						if (isset($allow_files[$output_file_name])) {
+							echoFilePre($output_file_name);
+						} else {
+							echoFileNotFound($output_file_name);
+						}
+						echo '</div>';
+						echo '</div>';
+					}
+				}
+			);
+			if ($has_extra_tests) {
+				$n_ex_tests = getUOJConfVal($problem_conf, 'n_ex_tests', 0);
+				if (!validateUInt($n_ex_tests)) {
+					return (new DataDisplayer($problem_conf))->setProblemConfRowStatus('n_ex_tests', 'danger');
+				}
+
+				$data_disp->addDisplayer('extra tests',
+					function($self) use ($problem_conf, $allow_files, $n_tests, $n_ex_tests) {
+						for ($num = 1; $num <= $n_ex_tests; $num++) {
+							$input_file_name = getUOJProblemExtraInputFileName($problem_conf, $num);
+							$output_file_name = getUOJProblemExtraOutputFileName($problem_conf, $num);
+							echo '<div class="row">';
+							echo '<div class="col-md-6">';
+							if (isset($allow_files[$input_file_name])) {
+								echoFilePre($input_file_name);
+							} else {
+								echoFileNotFound($input_file_name);
+							}
+							echo '</div>';
+							echo '<div class="col-md-6">';
+							if (isset($allow_files[$output_file_name])) {
+								echoFilePre($output_file_name);
+							} else {
+								echoFileNotFound($output_file_name);
+							}
+							echo '</div>';
+							echo '</div>';
+						}
+					}
+				);
+			}
+			
+			if (!isset($problem_conf['interaction_mode'])) {
+				if (isset($problem_conf['use_builtin_checker'])) {
+					$data_disp->addDisplayer('checker', function($self) {
+						echo '<h4>use builtin checker : ', $self->problem_conf['use_builtin_checker']['val'], '</h4>';
+					});
+				} else {
+					$data_disp->addDisplayer('checker', $getDisplaySrcFunc('chk'));
+				}
+			}
+			if ($problem['hackable']) {
+				$data_disp->addDisplayer('standard', $getDisplaySrcFunc('std'));
+				$data_disp->addDisplayer('validator', $getDisplaySrcFunc('val'));
+			}
+			if (isset($problem_conf['interaction_mode'])) {
+				$data_disp->addDisplayer('interactor', $getDisplaySrcFunc('interactor'));
+			}
+			return $data_disp;
+		} else {
+			return (new DataDisplayer($problem_conf))->setProblemConfRowStatus('use_builtin_judger', 'danger');
+		}
+	}
+
+	$data_disp = getDataDisplayer();
+
+	if (isset($_GET['display_file'])) {
+		if (!isset($_GET['file_name'])) {
+			echoFileNotFound('');
+		} else {
+			$data_disp->displayFile($_GET['file_name']);
+		}
+		die();
+	}
+
+	$hackable_form = new UOJForm('hackable');
+	$hackable_form->handle = function() {
+		global $problem;
+		$problem['hackable'] = !$problem['hackable'];
+		//$problem['hackable'] = 0;
+		$ret = dataSyncProblemData($problem);
+		if ($ret) {
+			becomeMsgPage('<div>' . $ret . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		}
+		
+		$hackable = $problem['hackable'] ? 1 : 0;
+		DB::query("update problems set hackable = $hackable where id = ${problem['id']}");
+	};
+	$hackable_form->submit_button_config['class_str'] = 'btn btn-warning btn-block';
+	$hackable_form->submit_button_config['text'] = $problem['hackable'] ? '禁止使用hack' : '允许使用hack';
+	$hackable_form->submit_button_config['smart_confirm'] = '';
+
+	$data_form = new UOJForm('data');
+	$data_form->handle = function() {
+		global $problem, $myUser;
+		set_time_limit(60 * 5);
+		$ret = dataSyncProblemData($problem, $myUser);
+		if ($ret) {
+			becomeMsgPage('<div>' . $ret . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		}
+	};
+	$data_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$data_form->submit_button_config['text'] = '检验配置并同步';
+	$data_form->submit_button_config['smart_confirm'] = '';
+	
+	$clear_data_form = new UOJForm('clear_data');
+	$clear_data_form->handle = function() {
+		global $problem;
+		dataClearProblemData($problem);
+	};
+	$clear_data_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$clear_data_form->submit_button_config['text'] = '清空题目数据';
+	$clear_data_form->submit_button_config['smart_confirm'] = '';
+
+	$rejudge_form = new UOJForm('rejudge');
+	$rejudge_form->handle = function() {
+		global $problem;
+		rejudgeProblem($problem);
+	};
+	$rejudge_form->succ_href = "/submissions?problem_id={$problem['id']}";
+	$rejudge_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$rejudge_form->submit_button_config['text'] = '重测该题';
+	$rejudge_form->submit_button_config['smart_confirm'] = '';
+	
+	$rejudgege97_form = new UOJForm('rejudgege97');
+	$rejudgege97_form->handle = function() {
+		global $problem;
+		rejudgeProblemGe97($problem);
+	};
+	$rejudgege97_form->succ_href = "/submissions?problem_id={$problem['id']}";
+	$rejudgege97_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$rejudgege97_form->submit_button_config['text'] = '重测 >=97 的程序';
+	$rejudgege97_form->submit_button_config['smart_confirm'] = '';
+	
+	$view_type_form = new UOJForm('view_type');
+	$view_type_form->addVSelect('view_content_type',
+		array('NONE' => '禁止',
+				'SELF' => '仅自己',
+				'ALL_AFTER_AC' => 'AC后',
+				'ALL' => '所有人'
+		),
+		'查看提交文件:',
+		$problem_extra_config['view_content_type']
+	);
+	$view_type_form->addVSelect('view_all_details_type',
+		array('NONE' => '禁止',
+				'SELF' => '仅自己',
+				'ALL_AFTER_AC' => 'AC后',
+				'ALL' => '所有人'
+		),
+		'查看全部详细信息:',
+		$problem_extra_config['view_all_details_type']
+	);
+	$view_type_form->addVSelect('view_details_type',
+		array('NONE' => '禁止',
+				'SELF' => '仅自己',
+				'ALL_AFTER_AC' => 'AC后',
+				'ALL' => '所有人'
+		),
+		'查看测试点详细信息:',
+		$problem_extra_config['view_details_type']
+	);
+	$view_type_form->handle = function() {
+		global $problem, $problem_extra_config;
+		$config = $problem_extra_config;
+		$config['view_content_type'] = $_POST['view_content_type'];
+		$config['view_all_details_type'] = $_POST['view_all_details_type'];
+		$config['view_details_type'] = $_POST['view_details_type'];
+		$esc_config = DB::escape(json_encode($config));
+		DB::query("update problems set extra_config = '$esc_config' where id = '{$problem['id']}'");
+	};
+	$view_type_form->submit_button_config['class_str'] = 'btn btn-warning btn-block top-buffer-sm';
+	
+	if ($problem['hackable']) {
+		$test_std_form = new UOJForm('test_std');
+		$test_std_form->handle = function() {
+			global $myUser, $problem;
+			
+			$user_std = queryUser('std');
+			if (!$user_std) {
+				becomeMsgPage('请建立"std"账号。');
+			}
+			
+			$requirement = json_decode($problem['submission_requirement'], true);
+			
+			$zip_file_name = uojRandAvaiableSubmissionFileName();
+			$zip_file = new ZipArchive();
+			if ($zip_file->open(UOJContext::storagePath().$zip_file_name, ZipArchive::CREATE) !== true) {
+				becomeMsgPage('提交失败');
+			}
+		
+			$content = array();
+			$content['file_name'] = $zip_file_name;
+			$content['config'] = array();
+			foreach ($requirement as $req) {
+				if ($req['type'] == "source code") {
+					$content['config'][] = array("{$req['name']}_language", "C++17");
+				}
+			}
+		
+			$tot_size = 0;
+			foreach ($requirement as $req) {
+				$zip_file->addFile("/var/uoj_data/{$problem['id']}/std.cpp", $req['file_name']);
+				$tot_size += $zip_file->statName($req['file_name'])['size'];
+			}
+		
+			$zip_file->close();
+		
+			$content['config'][] = array('validate_input_before_test', 'on');
+			$content['config'][] = array('problem_id', $problem['id']);
+			$esc_content = DB::escape(json_encode($content));
+			$esc_language = DB::escape('C++');
+		 	
+			$result = array();
+			$result['status'] = "Waiting";
+			$result_json = json_encode($result);
+			$is_hidden = $problem['is_hidden'] ? 1 : 0;
+			
+			DB::insert("insert into submissions (problem_id, submit_time, submitter, content, language, tot_size, status, result, is_hidden) values ({$problem['id']}, now(), '{$user_std['username']}', '$esc_content', '$esc_language', $tot_size, '{$result['status']}', '$result_json', $is_hidden)");
+		};
+		$test_std_form->succ_href = "/submissions?problem_id={$problem['id']}";
+		$test_std_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+		$test_std_form->submit_button_config['text'] = '检验数据正确性';
+		$test_std_form->runAtServer();
+	}
+	
+	$hackable_form->runAtServer();
+	$view_type_form->runAtServer();
+	$data_form->runAtServer();
+	$clear_data_form->runAtServer();
+	$rejudge_form->runAtServer();
+	$rejudgege97_form->runAtServer();
+	$info_form->runAtServer();
+?>
+<?php
+	$REQUIRE_LIB['dialog'] = '';
+?>
+<?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 数据 - 题目管理') ?>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<ul class="nav nav-tabs" role="tablist">
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>
+	<li class="nav-item"><a class="nav-link active" href="/problem/<?= $problem['id'] ?>/manage/data" role="tab">数据</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?=$problem['id']?>" role="tab">返回</a></li>
+</ul>
+
+<div class="row">
+	<div class="col-md-10 top-buffer-sm">
+		<div class="row">
+			<div class="col-md-3 top-buffer-sm" id="div-file_list">
+				<ul class="nav nav-pills flex-column">
+					<?php $data_disp->echoDataFilesList('problem.conf'); ?>
+				</ul>
+			</div>
+			<div class="col-md-9 top-buffer-sm" id="div-file_content">
+				<?php $data_disp->displayFile('problem.conf'); ?>
+			</div>
+			<script type="text/javascript">
+				curFileName = '';
+				$('#div-file_list a').click(function(e) {
+					$('#div-file_content').html('<h3>Loading...</h3>');
+					$(this).tab('show');
+
+					var fileName = $(this).text();
+					curFileName = fileName;
+					$.get('/problem/<?= $problem['id'] ?>/manage/data', {
+							display_file: '',
+							file_name: fileName
+						},
+						function(data) {
+							if (curFileName != fileName) {
+								return;
+							}
+							$('#div-file_content').html(data);
+						},
+						'html'
+					);
+					return false;
+				});
+			</script>
+		</div>
+	</div>
+	<div class="col-md-2 top-buffer-sm">
+		<div class="top-buffer-md">
+			<?php if ($problem['hackable']): ?>
+				<span class="glyphicon glyphicon-ok"></span> hack功能已启用
+			<?php else: ?>
+				<span class="glyphicon glyphicon-remove"></span> hack功能已禁止
+			<?php endif ?>
+			<?php $hackable_form->printHTML() ?>
+		</div>
+		<div class="top-buffer-md">
+		<?php if ($problem['hackable']): ?>
+			<?php $test_std_form->printHTML() ?>
+		<?php endif ?>
+		</div>
+		<div class="top-buffer-md">
+			<button id="button-display_view_type" type="button" class="btn btn-primary btn-block" onclick="$('#div-view_type').toggle('fast');">提交记录可视权限</button>
+			<div class="top-buffer-sm" id="div-view_type" style="display:none; padding-left:5px; padding-right:5px;">
+				<?php $view_type_form->printHTML(); ?>
+			</div>
+		</div>
+		<div class="top-buffer-md">
+			<?php $data_form->printHTML(); ?>
+		</div>
+		<div class="top-buffer-md">
+			<?php $clear_data_form->printHTML(); ?>
+		</div>
+		<div class="top-buffer-md">
+			<?php $rejudge_form->printHTML(); ?>
+		</div>
+		<div class="top-buffer-md">
+			<?php $rejudgege97_form->printHTML(); ?>
+		</div>
+
+		<div class="top-buffer-md">
+			<button type="button" class="btn btn-block btn-primary" data-toggle="modal" data-target="#UploadDataModal">上传数据</button>
+		</div>
+		<div class="top-buffer-md">
+			<button type="button" class="btn btn-block btn-primary" data-toggle="modal" data-target="#ProblemSettingsFileModal">试题配置</button>
+		</div>
+	</div>
+
+	<div class="modal fade" id="UploadDataModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+  		<div class="modal-dialog">
+    			<div class="modal-content">
+      				<div class="modal-header">
+						<h4 class="modal-title" id="myModalLabel">上传数据</h4>
+        				<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+      				</div>
+      				<div class="modal-body">
+        				<form action="" method="post" enctype="multipart/form-data" role="form">
+							<div class="form-group">
+									<label for="exampleInputFile">上传zip文件</label>
+									<input type="file" name="problem_data_file" id="problem_data_file">
+									<p class="help-block">说明:请将所有数据放置于压缩包根目录内。若压缩包内仅存在文件夹而不存在文件,则会将这些一级子文件夹下的内容移动到根目录下,然后这些一级子文件夹删除;若这些子文件夹内存在同名文件,则会发生随机替换,仅保留一个副本。
+<?php 
+	echo "<br>本组剩余空间配额:<b class='text-danger'>" 
+					. printSize(queryGroupQuota($group) - queryGroupTotalSize($group))
+									?></b></p>
+							</div>
+							<input type="hidden" name="problem_data_file_submit" value="submit">
+      				</div>
+      				<div class="modal-footer">
+						<button type="submit" class="btn btn-success">上传</button>
+						</form>
+        				<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+      				</div>
+    			</div>
+  		</div>
+	</div>
+
+	<div class="modal fade" id="ProblemSettingsFileModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+  		<div class="modal-dialog">
+    			<div class="modal-content">
+      				<div class="modal-header">
+						<h4 class="modal-title" id="myModalLabel">试题配置</h4>
+        				<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+      				</div>
+      				<div class="modal-body">
+        				<form class="form-horizontal" action="" method="post" role="form">
+        					<div class="form-group row">
+    							<label for="use_builtin_checker" class="col-sm-5 control-label">比对函数</label>
+    							<div class="col-sm-7">
+								<select class="form-control" id="use_builtin_checker" name="use_builtin_checker">
+  									<option value="ncmp">单行整数序列</option>
+  									<option value="wcmp">单行字符串序列</option>
+  									<option value="fcmp">多行数据(不忽略行末空格,但忽略文末回车)</option>
+									<option value="ownchk">自定义校验器</option>
+								</select>
+      								<!--<input type="hidden" class="form-control" id="use_builtin_checker" name="use_builtin_checker" placeholder="比对函数">-->
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="n_tests" class="col-sm-5 control-label">n_tests</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="n_tests" name="n_tests" placeholder="数据点个数">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="n_ex_tests" class="col-sm-5 control-label">n_ex_tests</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="n_ex_tests" name="n_ex_tests" placeholder="额外数据点个数">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="n_sample_tests" class="col-sm-5 control-label">n_sample_tests</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="n_sample_tests" name="n_sample_tests" placeholder="样例测试点个数">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="input_pre" class="col-sm-5 control-label">input_pre</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="input_pre" name="input_pre" placeholder="输入文件名称">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="input_suf" class="col-sm-5 control-label">input_suf</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="input_suf" name="input_suf" placeholder="输入文件后缀">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="output_pre" class="col-sm-5 control-label">output_pre</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="output_pre" name="output_pre" placeholder="输出文件名称">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="output_suf" class="col-sm-5 control-label">output_suf</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="output_suf" name="output_suf" placeholder="输出文件后缀">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="time_limit" class="col-sm-5 control-label">time_limit</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="time_limit" name="time_limit" placeholder="时间限制(不能为小数!)">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="memory_limit" class="col-sm-5 control-label">memory_limit</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="memory_limit" name="memory_limit" placeholder="内存限制">
+    							</div>
+  							</div>
+							<input type="hidden" name="problem_settings_file_submit" value="submit">
+      				</div>
+      				<div class="modal-footer">
+						<button type="submit" class="btn btn-success">确定</button>
+						</form>
+        				<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+      				</div>
+    			</div>
+  		</div>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 821 - 0
app/controllers/problem_data_manage_back.php

@@ -0,0 +1,821 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	requirePHPLib('data');
+	
+	if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
+		become404Page();
+	}
+	if (!hasProblemCtrlPermission($myUser, $problem)) {
+		become403Page();
+	}
+	$group = queryUser($problem['ctrl']);
+	$oj_name = UOJConfig::$data['profile']['oj-name'];
+	$problem_extra_config = getProblemExtraConfig($problem);
+
+	$data_dir = "/var/uoj_data/${problem['id']}";
+
+	function echoFileNotFound($file_name) {
+		echo '<h4>', htmlspecialchars($file_name), '<sub class="text-danger"> ', '文件未找到', '</sub></h4>';
+	}
+	function echoFilePre($file_name) {
+		global $data_dir;
+		$file_full_name = $data_dir . '/' . $file_name;
+
+		$finfo = finfo_open(FILEINFO_MIME);
+		$mimetype = finfo_file($finfo, $file_full_name);
+		if ($mimetype === false) {
+			echoFileNotFound($file_name);
+			return;
+		}
+		finfo_close($finfo);
+
+		echo '<h4>', htmlspecialchars($file_name), '<sub> ', $mimetype, '</sub></h4>';
+		echo "<pre>\n";
+
+		$output_limit = 1000;
+		if (strStartWith($mimetype, 'text/')) {
+			echo htmlspecialchars(uojFilePreview($file_full_name, $output_limit));
+		} else {
+			echo htmlspecialchars(uojFilePreview($file_full_name, $output_limit, 'binary'));
+		}
+		echo "\n</pre>";
+	}
+
+
+	//上传数据
+	if ($_POST['problem_data_file_submit']=='submit') {
+		if ($_FILES["problem_data_file"]["error"] > 0) {
+			$errmsg = "Error: ".$_FILES["problem_data_file"]["error"];
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		} else {
+			$zip_mime_types = array('application/zip', 'application/x-zip', 'application/x-zip-compressed');
+			if (in_array($_FILES["problem_data_file"]["type"], $zip_mime_types) || $_FILES["problem_data_file"]["type"] == 'application/octet-stream' && substr($_FILES["problem_data_file"]["name"], -4) == '.zip') {
+				$up_filename="/tmp/".rand(0,100000000)."data.zip";
+				move_uploaded_file($_FILES["problem_data_file"]["tmp_name"], $up_filename);
+				if (queryGroupTotalSize($group) + filesize($up_filename) > queryGroupQuota($group)) {
+					$errmsg = '组空间配额超限。';
+					unlink($up_filename);
+					becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+				}
+
+				$zip = new ZipArchive;
+				if ($zip->open($up_filename) === TRUE) {
+					$zip->extractTo("/var/uoj_data/upload/{$problem['id']}");
+					$zip->close();
+					exec("cd /var/uoj_data/upload/{$problem['id']}; if [ -z \"`find . -maxdepth 1 -type f`\" ]; then for sub_dir in `find -maxdepth 1 -type d ! -name .`; do mv -f \$sub_dir/* . && rm -rf \$sub_dir; done; fi");
+					echo "<script>alert('上传成功!')</script>";
+				} else {
+					$errmsg = "解压失败!";
+					becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+				}
+				unlink($up_filename);
+			} else {
+				$errmsg = "请上传zip格式!";
+				becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+			}
+		}
+	}
+
+	//添加配置文件
+	if ($_POST['problem_settings_file_submit']=='submit') {
+		if ($_POST['use_builtin_checker'] and $_POST['n_tests'] and $_POST['input_pre'] and $_POST['input_suf'] and $_POST['output_pre'] and $_POST['output_suf'] and $_POST['time_limit'] and $_POST['memory_limit']) {
+			$set_filename="/var/uoj_data/upload/{$problem['id']}/problem.conf";
+			$has_legacy=false;
+			if (file_exists($set_filename)) {
+				$has_legacy=true;
+				unlink($set_filename);
+			}
+			$setfile = fopen($set_filename, "w");
+			fwrite($setfile, "use_builtin_judger on\n");
+			if ($_POST['use_builtin_checker'] != 'ownchk') {
+				fwrite($setfile, "use_builtin_checker ".$_POST['use_builtin_checker']."\n");
+			}
+			fwrite($setfile, "n_tests ".$_POST['n_tests']."\n");
+			if ($_POST['n_ex_tests']) {
+				fwrite($setfile, "n_ex_tests ".$_POST['n_ex_tests']."\n");
+			} else {
+				fwrite($setfile, "n_ex_tests 0\n");
+			}
+			if ($_POST['n_sample_tests']) {
+				fwrite($setfile, "n_sample_tests ".$_POST['n_sample_tests']."\n");
+			} else {
+				fwrite($setfile, "n_sample_tests 0\n");
+			}
+			fwrite($setfile, "input_pre ".$_POST['input_pre']."\n");
+			fwrite($setfile, "input_suf ".$_POST['input_suf']."\n");
+			fwrite($setfile, "output_pre ".$_POST['output_pre']."\n");
+			fwrite($setfile, "output_suf ".$_POST['output_suf']."\n");
+			fwrite($setfile, "time_limit ".$_POST['time_limit']."\n");
+			fwrite($setfile, "memory_limit ".$_POST['memory_limit']."\n");
+			fclose($setfile);
+			if (!$has_legacy) {
+				echo "<script>alert('添加成功!')</script>";
+			} else {
+				echo "<script>alert('替换成功!')</script>";
+			}
+		} else {
+			$errmsg = "添加配置文件失败,请检查是否所有输入框都已填写!";
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		}
+	}
+
+
+	$info_form = new UOJForm('info');
+	$http_host = HTML::escape(UOJContext::httpHost());
+	$download_url = HTML::url("/download.php?type=problem&id={$problem['id']}");
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<!--<label class="col-sm-3 control-label">zip上传数据</label>
+	<div class="col-sm-9">
+		<div class="form-control-static">
+			<row>
+			<button type="button" style="width:30%" class="btn btn-primary" data-toggle="modal" data-target="#UploadDataModal">上传数据</button>
+			<button type="submit" style="width:30%" id="button-submit-data" name="submit-data" value="data" class="btn btn-danger">检验配置并同步数据</button>
+			</row>
+		</div>
+	</div>-->
+</div>
+EOD
+	);
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">problem_{$problem['id']}.zip</label>
+	<div class="col-sm-9">
+		<div class="form-control-static">
+			<a href="$download_url">$download_url</a>
+		</div>
+	</div>
+</div>
+EOD
+	);
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">testlib.h</label>
+	<div class="col-sm-9">
+		<div class="form-control-static">
+			<a href="/download.php?type=testlib.h">下载</a>
+		</div>
+	</div>
+</div>
+EOD
+	);
+
+	$esc_submission_requirement = HTML::escape(json_encode(json_decode($problem['submission_requirement']), JSON_PRETTY_PRINT));
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">提交文件配置</label>
+	<div class="col-sm-9">
+		<div class="form-control-static"><pre>
+$esc_submission_requirement
+</pre>
+		</div>
+	</div>
+</div>
+EOD
+	);
+	$esc_extra_config = HTML::escape(json_encode(json_decode($problem['extra_config']), JSON_PRETTY_PRINT));
+	$info_form->appendHTML(<<<EOD
+<div class="form-group row">
+	<label class="col-sm-3 control-label">其它配置</label>
+	<div class="col-sm-9">
+		<div class="form-control-static"><pre>
+$esc_extra_config
+</pre>
+		</div>
+	</div>
+</div>
+EOD
+	);
+	if (isSuperUser($myUser)) {
+		$info_form->addVInput('submission_requirement', 'text', '提交文件配置', $problem['submission_requirement'],
+			function ($submission_requirement, &$vdata) {
+				$submission_requirement = json_decode($submission_requirement, true);
+				if ($submission_requirement === null) {
+					return '不是合法的JSON';
+				}
+				$vdata['submission_requirement'] = json_encode($submission_requirement);
+			},
+			null);
+		$info_form->addVInput('extra_config', 'text', '其它配置', $problem['extra_config'],
+			function ($extra_config, &$vdata) {
+				$extra_config = json_decode($extra_config, true);
+				if ($extra_config === null) {
+					return '不是合法的JSON';
+				}
+				$vdata['extra_config'] = json_encode($extra_config);
+			},
+			null);
+		$info_form->handle = function(&$vdata) {
+			global $problem;
+			$esc_submission_requirement = DB::escape($vdata['submission_requirement']);
+			$esc_extra_config = DB::escape($vdata['extra_config']);
+			DB::update("update problems set submission_requirement = '$esc_submission_requirement', extra_config = '$esc_extra_config' where id = {$problem['id']}");
+		};
+	} else {
+		$info_form->no_submit = true;
+	}
+
+	class DataDisplayer {
+		public $problem_conf = array();
+		public $data_files = array();
+		public $displayers = array();
+
+		public function __construct($problem_conf = null, $data_files = null) {
+			global $data_dir;
+
+			if (isset($problem_conf)) {
+				foreach ($problem_conf as $key => $val) {
+					$this->problem_conf[$key] = array('val' => $val);
+				}
+			}
+
+			if (!isset($data_files)) {
+				$this->data_files = array_filter(scandir($data_dir), function($x) {
+					return $x !== '.' && $x !== '..' && $x !== 'problem.conf';
+				});
+				natsort($this->data_files);
+				array_unshift($this->data_files, 'problem.conf');
+			} else {
+				$this->data_files = $data_files;
+			}
+
+			$this->setDisplayer('problem.conf', function($self) {
+				global $info_form;
+				$info_form->printHTML();
+				echo '<div class="top-buffer-md"></div>';
+
+				echo '<table class="table table-bordered table-hover table-striped table-text-center">';
+				echo '<thead>';
+				echo '<tr>';
+				echo '<th>key</th>';
+				echo '<th>value</th>';
+				echo '</tr>';
+				echo '</thead>';
+				echo '<tbody>';
+				foreach ($self->problem_conf as $key => $info) {
+					if (!isset($info['status'])) {
+						echo '<tr>';
+						echo '<td>', htmlspecialchars($key), '</td>';
+						echo '<td>', htmlspecialchars($info['val']), '</td>';
+						echo '</tr>';
+					} elseif ($info['status'] == 'danger') {
+						echo '<tr class="text-danger">';
+						echo '<td>', htmlspecialchars($key), '</td>';
+						echo '<td>', htmlspecialchars($info['val']), ' <span class="glyphicon glyphicon-remove"></span>', '</td>';
+						echo '</tr>';
+					}
+				}
+				echo '</tbody>';
+				echo '</table>';
+
+				echoFilePre('problem.conf');
+			});
+		}
+
+		public function setProblemConfRowStatus($key, $status) {
+			$this->problem_conf[$key]['status'] = $status;
+			return $this;
+		}
+
+		public function setDisplayer($file_name, $fun) {
+			$this->displayers[$file_name] = $fun;
+			return $this;
+		}
+		public function addDisplayer($file_name, $fun) {
+			$this->data_files[] = $file_name;
+			$this->displayers[$file_name] = $fun;
+			return $this;
+		}
+		public function echoDataFilesList($active_file) {
+			foreach ($this->data_files as $file_name) {
+				echo '<li class="nav-item">';
+				if ($file_name != $active_file) {
+					echo '<a class="nav-link" href="#">';
+				} else {
+					echo '<a class="nav-link active" href="#">';
+				}
+				echo htmlspecialchars($file_name), '</a>', '</li>';
+			}
+		}
+		public function displayFile($file_name) {
+			global $data_dir;
+
+			if (isset($this->displayers[$file_name])) {
+				$fun = $this->displayers[$file_name];
+				$fun($this);
+			} elseif (in_array($file_name, $this->data_files)) {
+				echoFilePre($file_name);
+			} else {
+				echoFileNotFound($file_name);
+			}
+		}
+	}
+
+	function getDataDisplayer() {
+		global $data_dir;
+		global $problem;
+
+		$allow_files = array_flip(array_filter(scandir($data_dir), function($x) {
+			return $x !== '.' && $x !== '..';
+		}));
+
+		$getDisplaySrcFunc = function($name) use ($allow_files) {
+			return function() use ($name, $allow_files) {
+				$src_name = $name . '.cpp';
+				if (isset($allow_files[$src_name])) {
+					echoFilePre($src_name);
+				} else {
+					echoFileNotFound($src_name);
+				}
+				if (isset($allow_files[$name])) {
+					echoFilePre($name);
+				} else {
+					echoFileNotFound($name);
+				}
+			};
+		};
+
+		$problem_conf = getUOJConf("$data_dir/problem.conf");
+		if ($problem_conf === -1) {
+			return (new DataDisplayer())->setDisplayer('problem.conf', function() {
+				global $info_form;
+				$info_form->printHTML();
+				echoFileNotFound('problem.conf');
+			});
+		}
+		if ($problem_conf === -2) {
+			return (new DataDisplayer())->setDisplayer('problem.conf', function() {
+				global $info_form;
+				$info_form->printHTML();
+				echo '<h4 class="text-danger">problem.conf 格式有误</h4>';
+				echoFilePre('problem.conf');
+			});
+		}
+
+		$judger_name = getUOJConfVal($problem_conf, 'use_builtin_judger', null);
+		if (!isset($problem_conf['use_builtin_judger'])) {
+			return new DataDisplayer($problem_conf);
+		}
+		if ($problem_conf['use_builtin_judger'] == 'on') {
+			$n_tests = getUOJConfVal($problem_conf, 'n_tests', 10);
+			if (!validateUInt($n_tests)) {
+				return (new DataDisplayer($problem_conf))->setProblemConfRowStatus('n_tests', 'danger');
+			}
+
+			$has_extra_tests = !(isset($problem_conf['submit_answer']) && $problem_conf['submit_answer'] == 'on');
+
+			$data_disp = new DataDisplayer($problem_conf, array('problem.conf'));
+			$data_disp->addDisplayer('tests',
+				function($self) use ($problem_conf, $allow_files, $n_tests, $n_ex_tests) {
+					for ($num = 1; $num <= $n_tests; $num++) {
+						$input_file_name = getUOJProblemInputFileName($problem_conf, $num);
+						$output_file_name = getUOJProblemOutputFileName($problem_conf, $num);
+						echo '<div class="row">';
+						echo '<div class="col-md-6">';
+						if (isset($allow_files[$input_file_name])) {
+							echoFilePre($input_file_name);
+						} else {
+							echoFileNotFound($input_file_name);
+						}
+						echo '</div>';
+						echo '<div class="col-md-6">';
+						if (isset($allow_files[$output_file_name])) {
+							echoFilePre($output_file_name);
+						} else {
+							echoFileNotFound($output_file_name);
+						}
+						echo '</div>';
+						echo '</div>';
+					}
+				}
+			);
+			if ($has_extra_tests) {
+				$n_ex_tests = getUOJConfVal($problem_conf, 'n_ex_tests', 0);
+				if (!validateUInt($n_ex_tests)) {
+					return (new DataDisplayer($problem_conf))->setProblemConfRowStatus('n_ex_tests', 'danger');
+				}
+
+				$data_disp->addDisplayer('extra tests',
+					function($self) use ($problem_conf, $allow_files, $n_tests, $n_ex_tests) {
+						for ($num = 1; $num <= $n_ex_tests; $num++) {
+							$input_file_name = getUOJProblemExtraInputFileName($problem_conf, $num);
+							$output_file_name = getUOJProblemExtraOutputFileName($problem_conf, $num);
+							echo '<div class="row">';
+							echo '<div class="col-md-6">';
+							if (isset($allow_files[$input_file_name])) {
+								echoFilePre($input_file_name);
+							} else {
+								echoFileNotFound($input_file_name);
+							}
+							echo '</div>';
+							echo '<div class="col-md-6">';
+							if (isset($allow_files[$output_file_name])) {
+								echoFilePre($output_file_name);
+							} else {
+								echoFileNotFound($output_file_name);
+							}
+							echo '</div>';
+							echo '</div>';
+						}
+					}
+				);
+			}
+			
+			if (!isset($problem_conf['interaction_mode'])) {
+				if (isset($problem_conf['use_builtin_checker'])) {
+					$data_disp->addDisplayer('checker', function($self) {
+						echo '<h4>use builtin checker : ', $self->problem_conf['use_builtin_checker']['val'], '</h4>';
+					});
+				} else {
+					$data_disp->addDisplayer('checker', $getDisplaySrcFunc('chk'));
+				}
+			}
+			if ($problem['hackable']) {
+				$data_disp->addDisplayer('standard', $getDisplaySrcFunc('std'));
+				$data_disp->addDisplayer('validator', $getDisplaySrcFunc('val'));
+			}
+			if (isset($problem_conf['interaction_mode'])) {
+				$data_disp->addDisplayer('interactor', $getDisplaySrcFunc('interactor'));
+			}
+			return $data_disp;
+		} else {
+			return (new DataDisplayer($problem_conf))->setProblemConfRowStatus('use_builtin_judger', 'danger');
+		}
+	}
+
+	$data_disp = getDataDisplayer();
+
+	if (isset($_GET['display_file'])) {
+		if (!isset($_GET['file_name'])) {
+			echoFileNotFound('');
+		} else {
+			$data_disp->displayFile($_GET['file_name']);
+		}
+		die();
+	}
+
+	$hackable_form = new UOJForm('hackable');
+	$hackable_form->handle = function() {
+		global $problem;
+		$problem['hackable'] = !$problem['hackable'];
+		//$problem['hackable'] = 0;
+		$ret = dataSyncProblemData($problem);
+		if ($ret) {
+			becomeMsgPage('<div>' . $ret . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		}
+		
+		$hackable = $problem['hackable'] ? 1 : 0;
+		DB::query("update problems set hackable = $hackable where id = ${problem['id']}");
+	};
+	$hackable_form->submit_button_config['class_str'] = 'btn btn-warning btn-block';
+	$hackable_form->submit_button_config['text'] = $problem['hackable'] ? '禁止使用hack' : '允许使用hack';
+	$hackable_form->submit_button_config['smart_confirm'] = '';
+
+	$data_form = new UOJForm('data');
+	$data_form->handle = function() {
+		global $problem, $myUser;
+		set_time_limit(60 * 5);
+		$ret = dataSyncProblemData($problem, $myUser);
+		if ($ret) {
+			becomeMsgPage('<div>' . $ret . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
+		}
+	};
+	$data_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$data_form->submit_button_config['text'] = '检验配置并同步数据';
+	$data_form->submit_button_config['smart_confirm'] = '';
+	
+	$clear_data_form = new UOJForm('clear_data');
+	$clear_data_form->handle = function() {
+		global $problem;
+		dataClearProblemData($problem);
+	};
+	$clear_data_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$clear_data_form->submit_button_config['text'] = '清空题目数据';
+	$clear_data_form->submit_button_config['smart_confirm'] = '';
+
+	$rejudge_form = new UOJForm('rejudge');
+	$rejudge_form->handle = function() {
+		global $problem;
+		rejudgeProblem($problem);
+	};
+	$rejudge_form->succ_href = "/submissions?problem_id={$problem['id']}";
+	$rejudge_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$rejudge_form->submit_button_config['text'] = '重测该题';
+	$rejudge_form->submit_button_config['smart_confirm'] = '';
+	
+	$rejudgege97_form = new UOJForm('rejudgege97');
+	$rejudgege97_form->handle = function() {
+		global $problem;
+		rejudgeProblemGe97($problem);
+	};
+	$rejudgege97_form->succ_href = "/submissions?problem_id={$problem['id']}";
+	$rejudgege97_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+	$rejudgege97_form->submit_button_config['text'] = '重测 >=97 的程序';
+	$rejudgege97_form->submit_button_config['smart_confirm'] = '';
+	
+	$view_type_form = new UOJForm('view_type');
+	$view_type_form->addVSelect('view_content_type',
+		array('NONE' => '禁止',
+				'SELF' => '仅自己',
+				'ALL_AFTER_AC' => 'AC后',
+				'ALL' => '所有人'
+		),
+		'查看提交文件:',
+		$problem_extra_config['view_content_type']
+	);
+	$view_type_form->addVSelect('view_all_details_type',
+		array('NONE' => '禁止',
+				'SELF' => '仅自己',
+				'ALL_AFTER_AC' => 'AC后',
+				'ALL' => '所有人'
+		),
+		'查看全部详细信息:',
+		$problem_extra_config['view_all_details_type']
+	);
+	$view_type_form->addVSelect('view_details_type',
+		array('NONE' => '禁止',
+				'SELF' => '仅自己',
+				'ALL_AFTER_AC' => 'AC后',
+				'ALL' => '所有人'
+		),
+		'查看测试点详细信息:',
+		$problem_extra_config['view_details_type']
+	);
+	$view_type_form->handle = function() {
+		global $problem, $problem_extra_config;
+		$config = $problem_extra_config;
+		$config['view_content_type'] = $_POST['view_content_type'];
+		$config['view_all_details_type'] = $_POST['view_all_details_type'];
+		$config['view_details_type'] = $_POST['view_details_type'];
+		$esc_config = DB::escape(json_encode($config));
+		DB::query("update problems set extra_config = '$esc_config' where id = '{$problem['id']}'");
+	};
+	$view_type_form->submit_button_config['class_str'] = 'btn btn-warning btn-block top-buffer-sm';
+	
+	if ($problem['hackable']) {
+		$test_std_form = new UOJForm('test_std');
+		$test_std_form->handle = function() {
+			global $myUser, $problem;
+			
+			$user_std = queryUser('std');
+			if (!$user_std) {
+				becomeMsgPage('请建立"std"账号。');
+			}
+			
+			$requirement = json_decode($problem['submission_requirement'], true);
+			
+			$zip_file_name = uojRandAvaiableSubmissionFileName();
+			$zip_file = new ZipArchive();
+			if ($zip_file->open(UOJContext::storagePath().$zip_file_name, ZipArchive::CREATE) !== true) {
+				becomeMsgPage('提交失败');
+			}
+		
+			$content = array();
+			$content['file_name'] = $zip_file_name;
+			$content['config'] = array();
+			foreach ($requirement as $req) {
+				if ($req['type'] == "source code") {
+					$content['config'][] = array("{$req['name']}_language", "C++");
+				}
+			}
+		
+			$tot_size = 0;
+			foreach ($requirement as $req) {
+				$zip_file->addFile("/var/uoj_data/{$problem['id']}/std.cpp", $req['file_name']);
+				$tot_size += $zip_file->statName($req['file_name'])['size'];
+			}
+		
+			$zip_file->close();
+		
+			$content['config'][] = array('validate_input_before_test', 'on');
+			$content['config'][] = array('problem_id', $problem['id']);
+			$esc_content = DB::escape(json_encode($content));
+			$esc_language = DB::escape('C++');
+		 	
+			$result = array();
+			$result['status'] = "Waiting";
+			$result_json = json_encode($result);
+			$is_hidden = $problem['is_hidden'] ? 1 : 0;
+			
+			DB::insert("insert into submissions (problem_id, submit_time, submitter, content, language, tot_size, status, result, is_hidden) values ({$problem['id']}, now(), '{$user_std['username']}', '$esc_content', '$esc_language', $tot_size, '{$result['status']}', '$result_json', $is_hidden)");
+		};
+		$test_std_form->succ_href = "/submissions?problem_id={$problem['id']}";
+		$test_std_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
+		$test_std_form->submit_button_config['text'] = '检验数据正确性';
+		$test_std_form->runAtServer();
+	}
+	
+	$hackable_form->runAtServer();
+	$view_type_form->runAtServer();
+	$data_form->runAtServer();
+	$clear_data_form->runAtServer();
+	$rejudge_form->runAtServer();
+	$rejudgege97_form->runAtServer();
+	$info_form->runAtServer();
+?>
+<?php
+	$REQUIRE_LIB['dialog'] = '';
+?>
+<?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 数据 - 题目管理') ?>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<ul class="nav nav-tabs" role="tablist">
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>
+	<li class="nav-item"><a class="nav-link active" href="/problem/<?= $problem['id'] ?>/manage/data" role="tab">数据</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?=$problem['id']?>" role="tab">返回</a></li>
+</ul>
+
+<div class="row">
+	<div class="col-md-10 top-buffer-sm">
+		<div class="row">
+			<div class="col-md-3 top-buffer-sm" id="div-file_list">
+				<ul class="nav nav-pills flex-column">
+					<?php $data_disp->echoDataFilesList('problem.conf'); ?>
+				</ul>
+			</div>
+			<div class="col-md-9 top-buffer-sm" id="div-file_content">
+				<?php $data_disp->displayFile('problem.conf'); ?>
+			</div>
+			<script type="text/javascript">
+				curFileName = '';
+				$('#div-file_list a').click(function(e) {
+					$('#div-file_content').html('<h3>Loading...</h3>');
+					$(this).tab('show');
+
+					var fileName = $(this).text();
+					curFileName = fileName;
+					$.get('/problem/<?= $problem['id'] ?>/manage/data', {
+							display_file: '',
+							file_name: fileName
+						},
+						function(data) {
+							if (curFileName != fileName) {
+								return;
+							}
+							$('#div-file_content').html(data);
+						},
+						'html'
+					);
+					return false;
+				});
+			</script>
+		</div>
+	</div>
+	<div class="col-md-2 top-buffer-sm">
+		<div class="top-buffer-md">
+			<?php if ($problem['hackable']): ?>
+				<span class="glyphicon glyphicon-ok"></span> hack功能已启用
+			<?php else: ?>
+				<span class="glyphicon glyphicon-remove"></span> hack功能已禁止
+			<?php endif ?>
+			<?php $hackable_form->printHTML() ?>
+		</div>
+		<div class="top-buffer-md">
+		<?php if ($problem['hackable']): ?>
+			<?php $test_std_form->printHTML() ?>
+		<?php endif ?>
+		</div>
+		<div class="top-buffer-md">
+			<button id="button-display_view_type" type="button" class="btn btn-primary btn-block" onclick="$('#div-view_type').toggle('fast');">提交记录可视权限</button>
+			<div class="top-buffer-sm" id="div-view_type" style="display:none; padding-left:5px; padding-right:5px;">
+				<?php $view_type_form->printHTML(); ?>
+			</div>
+		</div>
+		<div class="top-buffer-md">
+			<?php $data_form->printHTML(); ?>
+		</div>
+		<div class="top-buffer-md">
+			<?php $clear_data_form->printHTML(); ?>
+		</div>
+		<div class="top-buffer-md">
+			<?php $rejudge_form->printHTML(); ?>
+		</div>
+		<div class="top-buffer-md">
+			<?php $rejudgege97_form->printHTML(); ?>
+		</div>
+
+		<div class="top-buffer-md">
+			<button type="button" class="btn btn-block btn-primary" data-toggle="modal" data-target="#UploadDataModal">上传数据</button>
+		</div>
+		<div class="top-buffer-md">
+			<button type="button" class="btn btn-block btn-primary" data-toggle="modal" data-target="#ProblemSettingsFileModal">试题配置</button>
+		</div>
+	</div>
+
+	<div class="modal fade" id="UploadDataModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+  		<div class="modal-dialog">
+    			<div class="modal-content">
+      				<div class="modal-header">
+						<h4 class="modal-title" id="myModalLabel">上传数据</h4>
+        				<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+      				</div>
+      				<div class="modal-body">
+        				<form action="" method="post" enctype="multipart/form-data" role="form">
+							<div class="form-group">
+									<label for="exampleInputFile">上传zip文件</label>
+									<input type="file" name="problem_data_file" id="problem_data_file">
+									<p class="help-block">说明:请将所有数据放置于压缩包根目录内。若压缩包内仅存在文件夹而不存在文件,则会将这些一级子文件夹下的内容移动到根目录下,然后这些一级子文件夹删除;若这些子文件夹内存在同名文件,则会发生随机替换,仅保留一个副本。
+<?php 
+	echo "<br>本组剩余空间配额:<b class='text-danger'>" 
+					. printSize(queryGroupQuota($group) - queryGroupTotalSize($group))
+									?></b></p>
+							</div>
+							<input type="hidden" name="problem_data_file_submit" value="submit">
+      				</div>
+      				<div class="modal-footer">
+						<button type="submit" class="btn btn-success">上传</button>
+						</form>
+        				<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+      				</div>
+    			</div>
+  		</div>
+	</div>
+
+	<div class="modal fade" id="ProblemSettingsFileModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+  		<div class="modal-dialog">
+    			<div class="modal-content">
+      				<div class="modal-header">
+						<h4 class="modal-title" id="myModalLabel">试题配置</h4>
+        				<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+      				</div>
+      				<div class="modal-body">
+        				<form class="form-horizontal" action="" method="post" role="form">
+        					<div class="form-group row">
+    							<label for="use_builtin_checker" class="col-sm-5 control-label">比对函数</label>
+    							<div class="col-sm-7">
+								<select class="form-control" id="use_builtin_checker" name="use_builtin_checker">
+  									<option value="ncmp">单行整数序列</option>
+  									<option value="wcmp">单行字符串序列</option>
+  									<option value="fcmp">多行数据(不忽略行末空格,但忽略文末回车)</option>
+									<option value="ownchk">自定义校验器</option>
+								</select>
+      								<!--<input type="hidden" class="form-control" id="use_builtin_checker" name="use_builtin_checker" placeholder="比对函数">-->
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="n_tests" class="col-sm-5 control-label">n_tests</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="n_tests" name="n_tests" placeholder="数据点个数">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="n_ex_tests" class="col-sm-5 control-label">n_ex_tests</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="n_ex_tests" name="n_ex_tests" placeholder="额外数据点个数">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="n_sample_tests" class="col-sm-5 control-label">n_sample_tests</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="n_sample_tests" name="n_sample_tests" placeholder="样例测试点个数">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="input_pre" class="col-sm-5 control-label">input_pre</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="input_pre" name="input_pre" placeholder="输入文件名称">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="input_suf" class="col-sm-5 control-label">input_suf</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="input_suf" name="input_suf" placeholder="输入文件后缀">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="output_pre" class="col-sm-5 control-label">output_pre</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="output_pre" name="output_pre" placeholder="输出文件名称">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="output_suf" class="col-sm-5 control-label">output_suf</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="output_suf" name="output_suf" placeholder="输出文件后缀">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="time_limit" class="col-sm-5 control-label">time_limit</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="time_limit" name="time_limit" placeholder="时间限制(不能为小数!)">
+    							</div>
+  							</div>
+  							<div class="form-group row">
+    							<label for="memory_limit" class="col-sm-5 control-label">memory_limit</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="memory_limit" name="memory_limit" placeholder="内存限制">
+    							</div>
+  							</div>
+							<input type="hidden" name="problem_settings_file_submit" value="submit">
+      				</div>
+      				<div class="modal-footer">
+						<button type="submit" class="btn btn-success">确定</button>
+						</form>
+        				<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+      				</div>
+    			</div>
+  		</div>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 59 - 0
app/controllers/problem_managers_manage.php

@@ -0,0 +1,59 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
+		become404Page();
+	}
+	if (!hasProblemCtrlPermission($myUser, $problem)) {
+		become403Page();
+	}
+	
+	$managers_form = newAddDelCmdForm('managers',
+		function($username) {
+			if (!validateUsername($username) || !queryUser($username)) {
+				return "不存在名为{$username}的用户";
+			}
+			return '';
+		},
+		function($type, $username) {
+			global $problem;
+			if ($type == '+') {
+				DB::query("insert into problems_permissions (problem_id, username) values (${problem['id']}, '$username')");
+			} elseif ($type == '-') {
+				DB::query("delete from problems_permissions where problem_id = ${problem['id']} and username = '$username'");
+			}
+		}
+	);
+	
+	$managers_form->runAtServer();
+?>
+<?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 管理者 - 题目管理') ?>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<ul class="nav nav-tabs" role="tablist">
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
+	<li class="nav-item"><a class="nav-link active" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/data" role="tab">数据</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?=$problem['id']?>" role="tab">返回</a></li>
+</ul>
+
+<table class="table table-hover">
+	<thead>
+		<tr>
+			<th>#</th>
+			<th>用户名</th>
+		</tr>
+	</thead>
+	<tbody>
+<?php
+	$row_id = 0;
+	$result = DB::query("select username from problems_permissions where problem_id = ${problem['id']}");
+	while ($row = DB::fetch($result, MYSQLI_ASSOC)) {
+		$row_id++;
+		echo '<tr>', '<td>', $row_id, '</td>', '<td>', getUserLink($row['username']), '</td>', '</tr>';
+	}
+?>
+	</tbody>
+</table>
+<p class="text-center">命令格式:命令一行一个,+mike表示把mike加入管理者,-mike表示把mike从管理者中移除</p>
+<?php $managers_form->printHTML(); ?>
+<?php echoUOJPageFooter() ?>

+ 372 - 0
app/controllers/problem_set.php

@@ -0,0 +1,372 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	requirePHPLib('data');
+	
+	if ($_POST['migrate_problem_submit'] == 'submit') {
+		$group = queryUser(DB::escape($_POST['target_group']));
+		$start = $_POST['start_pid'];
+		$end = $_POST['end_pid'];
+
+		if (!validateUInt($start) || !validateUInt($end)) {
+			$errmsg = 'Error: 不合法起始终止题号';
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
+		}
+
+		if (!group) {
+			$errmsg = "Error: 未找到组 {$_POST['target_group']}";
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
+		}
+
+		if (!hasGroupCtrlPermission($myUser, $group)) {
+			$errmsg = "Error: 你对组 {$group['username']} 没有控制权";
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
+		}
+
+		for ($i = $start; $i <= $end; $i++) {
+			if (!hasProblemCtrlPermission($myUser, queryProblemBrief($i))) {
+				$errmsg = "Error: 你对题目 $i 没有控制权";
+				becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
+			}
+		}
+
+		for ($i = $start; $i <= $end; $i++) {
+			attachProblemToGroup(queryProblemBrief($i), $group);
+			DB::update("update problems set ctrl = '{$group['username']}' where id = $i");
+		}
+	}
+
+	if ($_POST['copy_problem_submit'] == 'submit') {
+		$problems = explode(',', $_POST['copy_problem_list']);
+		if (count($problems) > 10) {
+			$errmsg = "Error: 你不能一次复制超过 10 个题目";
+			becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
+		}
+
+		foreach ($problems as $pid)
+			if (!hasProblemCtrlPermission($myUser, queryProblemBrief($pid))) {
+				$errmsg = "Error: 你对题目 $i 没有控制权";
+				becomeMsgPage('<div>' . $errmsg . '</div><a href="/problems">返回</a>');
+			}
+		foreach ($problems as $pid) {
+			$p = queryProblemBrief($pid);
+			$title = DB::escape($p['title']);
+
+			$q = queryProblemContent($pid);
+			$stat = DB::escape($q['statement']);
+			$statmd = DB::escape($q['statement_md']);
+			DB::query("insert into problems (title, is_hidden, submission_requirement, ctrl) values ('$title', {$p['is_hidden']}, '{$p['submission_requirement']}', '{$p['ctrl']}')");
+			$id = DB::insert_id();
+			DB::query("insert into problems_contents (id, statement, statement_md) values ($id, '$stat', '$statmd')");
+			dataCopyProblemData($pid, $id);
+			attachProblemToGroup(queryProblemBrief($id), queryUser($p['ctrl']));
+		}
+	}
+
+	function echoProblem($problem) {
+		global $myUser;
+		if (isProblemVisibleToUser($problem, $myUser)) {
+			echo '<tr class="text-center">';
+			if ($problem['submission_id']) {
+				echo '<td class="table-success">';
+			} else {
+				echo '<td>';
+			}
+			echo '#', $problem['id'], '</td>';
+			echo '<td class="text-left">';
+			if ($problem['is_hidden']) {
+				echo ' <span class="text-danger">[隐藏]</span> ';	
+			}
+			echo '<a href="/problem/', $problem['id'], '">', $problem['title'], '</a>';
+			if (isset($_COOKIE['show_tags_mode'])) {
+				foreach (queryProblemTags($problem['id']) as $tag) {
+					echo '<a class="uoj-problem-tag">', '<span class="badge badge-pill badge-secondary">', HTML::escape($tag), '</span>', '</a>';
+				}
+			}
+			echo '</td>';
+			if (isset($_COOKIE['show_submit_mode'])) {
+				$perc = $problem['submit_num'] > 0 ? round(100 * $problem['ac_num'] / $problem['submit_num']) : 0;
+				echo <<<EOD
+				<td><a href="/submissions?problem_id={$problem['id']}&min_score=100&max_score=100">&times;{$problem['ac_num']}</a></td>
+				<td><a href="/submissions?problem_id={$problem['id']}">&times;{$problem['submit_num']}</a></td>
+				<td>
+					<div class="progress bot-buffer-no">
+						<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="$perc" aria-valuemin="0" aria-valuemax="100" style="width: $perc%; min-width: 20px;">{$perc}%</div>
+					</div>
+				</td>
+EOD;
+			}
+			echo '<td class="text-middle">', getClickZanBlock('P', $problem['id'], $problem['zan']), '</td>';
+			echo '</tr>';
+		}
+	}
+	
+	$cond = array();
+	
+	$search_tag = null;
+	
+	$cur_tab = isset($_GET['tab']) ? $_GET['tab'] : 'all';
+	if ($cur_tab == 'template') {
+		$search_tag = "模板题";
+	}
+	if (isset($_GET['tag'])) {
+		$search_tag = $_GET['tag'];
+	}
+	if ($search_tag) {
+		$cond[] = "'".DB::escape($search_tag)."' in (select tag from problems_tags where problems_tags.problem_id = problems.id)";
+	}
+	if (isset($_GET["search"])) {
+		$cond[]="title like '%".DB::escape($_GET["search"])."%' or id like '%".DB::escape($_GET["search"])."%'";
+	}
+
+	if (isset($_GET['group'])) {
+		$groupname = $_GET['group'];
+		$cur_tab = $groupname;
+		$group = queryUser($groupname);
+		if ($group == null) become404Page();
+		if (!in_array($groupname, queryUserExtgroups($myUser))) become403Page();
+		$cond[] = "find_in_set(id, (select privs from user_info where username = '$groupname'))";
+	}
+	else {
+		$group = queryUser('group_default');
+		$groupname = 'group_default';
+	}
+
+	if ($cond) {
+		$cond = join($cond, ' and ');
+	} else {
+		$cond = '1';
+	}
+
+	$print_new_problem_form = false;
+	$print_mig_problem_form = false;
+	$print_copy_problem_form = false;
+	$print_datum_button = false;
+/*
+	if (($cur_tab == 'all' || $cur_tab == 'template') and isSuperUser($myUser)) {
+		$print_new_problem_form = true;
+		$new_problem_form = new UOJForm('new_problem');
+		$new_problem_form->handle = function() {
+			global $group;
+			DB::query("insert into problems (title, is_hidden, submission_requirement, ctrl) values ('New Problem', 1, '{}', 'group_default')");
+			$id = DB::insert_id();
+			DB::query("insert into problems_contents (id, statement, statement_md) values ($id, '', '')");
+			dataNewProblem($id);
+			attachProblemToGroup(queryProblemBrief($id), $group);
+		};
+		$new_problem_form->submit_button_config['align'] = 'right';
+		$new_problem_form->submit_button_config['class_str'] = 'btn btn-primary';
+		$new_problem_form->submit_button_config['text'] = UOJLocale::get('problems::add new');
+		$new_problem_form->submit_button_config['smart_confirm'] = '';
+		
+		$new_problem_form->runAtServer();
+	}
+ */
+	if (isSuperUser($myUser))
+		$print_datum_button = true;
+
+	if ($group != null and hasGroupCtrlPermission($myUser, $group)) {
+		$print_new_problem_form = true;
+		$print_copy_problem_form = true;
+
+		$new_problem_form = new UOJForm('new_problem');
+		$new_problem_form->handle = function() {
+			global $groupname;
+			global $group;
+			DB::query("insert into problems (title, is_hidden, submission_requirement, ctrl) values ('New Problem', 1, '{}', '" . $groupname . "')");
+			$id = DB::insert_id();
+			DB::query("insert into problems_contents (id, statement, statement_md) values ($id, '', '')");
+			dataNewProblem($id);
+			attachProblemToGroup(queryProblemBrief($id), $group);
+		};
+		$new_problem_form->submit_button_config['align'] = 'right';
+		$new_problem_form->submit_button_config['class_str'] = 'btn btn-primary';
+		$new_problem_form->submit_button_config['text'] = UOJLocale::get('problems::add new');
+		$new_problem_form->submit_button_config['smart_confirm'] = '';
+
+		$new_problem_form->runAtServer();
+
+		$print_mig_problem_form = true;
+	}
+	
+	$header = '<tr>';
+	$header .= '<th class="text-center" style="width:5em;">ID</th>';
+	$header .= '<th>'.UOJLocale::get('problems::problem').'</th>';
+	if (isset($_COOKIE['show_submit_mode'])) {
+		$header .= '<th class="text-center" style="width:5em;">'.UOJLocale::get('problems::ac').'</th>';
+		$header .= '<th class="text-center" style="width:5em;">'.UOJLocale::get('problems::submit').'</th>';
+		$header .= '<th class="text-center" style="width:150px;">'.UOJLocale::get('problems::ac rate').'</th>';
+	}
+	$header .= '<th class="text-center" style="width:220px;">'.UOJLocale::get('appraisal').'</th>';
+	$header .= '</tr>';
+	
+	$tabs_info = array(
+		'all' => array(
+			'name' => UOJLocale::get('problems::all problems'),
+			'url' => "/problems"
+		),
+		'template' => array(
+			'name' => UOJLocale::get('problems::template problems'),
+			'url' => "/problems/template"
+		)
+	);
+
+	foreach (queryUserExtgroups($myUser) as $i => $grp)
+		if ($grp != '')
+			$tabs_info[$grp] = array(
+				'name' => '团队 ' . $grp .' 题目',
+				'url' => "/problems/group/" . $grp
+			);
+	
+
+	$pag_config = array('page_len' => 100);
+	$pag_config['col_names'] = array('*');
+	$pag_config['table_name'] = "problems left join best_ac_submissions on best_ac_submissions.submitter = '{$myUser['username']}' and problems.id = best_ac_submissions.problem_id";
+	$pag_config['cond'] = $cond;
+	$pag_config['tail'] = "order by id asc";
+	$pag = new Paginator($pag_config);
+
+	$div_classes = array('table-responsive', 'shadow');
+	$table_classes = array('table', 'table-bordered', 'table-hover', 'table-striped');
+?>
+<?php echoUOJPageHeader(UOJLocale::get('problems')) ?>
+<div class="row my-4">
+	<div class="col-sm-9">
+		<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?>
+	</div>
+	<div class="col-sm-3 order-sm-9 checkbox text-right">
+		<label class="checkbox-inline" for="input-show_tags_mode"><input type="checkbox" id="input-show_tags_mode" <?= isset($_COOKIE['show_tags_mode']) ? 'checked="checked" ': ''?>/> <?= UOJLocale::get('problems::show tags') ?></label>
+		<label class="checkbox-inline" for="input-show_submit_mode"><input type="checkbox" id="input-show_submit_mode" <?= isset($_COOKIE['show_submit_mode']) ? 'checked="checked" ': ''?>/> <?= UOJLocale::get('problems::show statistics') ?></label>
+	</div>
+</div>
+<div class="top-buffer-sm"></div>
+<script type="text/javascript">
+$('#input-show_tags_mode').click(function() {
+	if (this.checked) {
+		$.cookie('show_tags_mode', '', {path: '/problems'});
+	} else {
+		$.removeCookie('show_tags_mode', {path: '/problems'});
+	}
+	location.reload();
+});
+$('#input-show_submit_mode').click(function() {
+	if (this.checked) {
+		$.cookie('show_submit_mode', '', {path: '/problems'});
+	} else {
+		$.removeCookie('show_submit_mode', {path: '/problems'});
+	}
+	location.reload();
+});
+</script>
+<?php
+	echo '<div class="', join($div_classes, ' '), '">';
+	echo '<table class="', join($table_classes, ' '), '">';
+	echo '<thead>';
+	echo $header;
+	echo '</thead>';
+	echo '<tbody>';
+	
+	foreach ($pag->get() as $idx => $row) {
+		echoProblem($row);
+		echo "\n";
+	}
+	if ($pag->isEmpty()) {
+		echo '<tr><td class="text-center" colspan="233">'.UOJLocale::get('none').'</td></tr>';
+	}
+	
+	echo '</tbody>';
+	echo '</table>';
+	echo '</div>';
+	
+	if ($print_new_problem_form)
+		$new_problem_form->printHTML();
+	if ($print_mig_problem_form) {
+	echo <<<EOF
+	<div class="text-right">
+		<button type="button" class="mt-2 btn btn-secondary" data-toggle="modal" data-target="#MigrateProblemModal">迁移题目</button>
+	</div>
+
+	<div class="modal fade" id="MigrateProblemModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+  		<div class="modal-dialog">
+    			<div class="modal-content">
+      				<div class="modal-header">
+					<h4 class="modal-title" id="myModalLabel">迁移题目</h4>
+        				<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+      				</div>
+      				<div class="modal-body">
+        				<form class="form-horizontal" action="" method="post" role="form">
+						<div class="form-group row">
+							<input type="hidden" name="migrate_problem_submit" value="submit">
+							<label for="target_group" class="col-sm-5 control-label">目标组</label>
+     							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="target_group" name="target_group" placeholder="group_default">
+							</div>
+   							<label for="start_pid" class="col-sm-5 control-label">起始题号</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="start_pid" name="start_pid" placeholder="您必须对范围内的">
+							</div>
+							<label for="end_pid" class="col-sm-5 control-label">终止题号</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="end_pid" name="end_pid" placeholder="所有题目都有控制权">
+							</div>
+						</div>
+      				</div>
+      				<div class="modal-footer">
+					<button type="submit" class="btn btn-success">确定</button>
+					</form>
+        				<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+      				</div>
+    			</div>
+  		</div>
+	</div>
+
+EOF;
+	}
+	if ($print_copy_problem_form) {
+	echo <<<EOF
+	<div class="text-right">
+		<button type="button" class="mt-2 btn btn-secondary" data-toggle="modal" data-target="#CopyProblemModal">复制题目</button>
+	</div>
+
+	<div class="modal fade" id="CopyProblemModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+  		<div class="modal-dialog">
+    			<div class="modal-content">
+      				<div class="modal-header">
+					<h4 class="modal-title" id="myModalLabel">复制题目</h4>
+        				<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+      				</div>
+      				<div class="modal-body">
+        				<form class="form-horizontal" action="" method="post" role="form">
+						<div class="form-group row">
+							<input type="hidden" name="copy_problem_submit" value="submit">
+   							<label for="copy_problem_list" class="col-sm-5 control-label">题目列表</label>
+    							<div class="col-sm-7">
+      								<input type="text" class="form-control" id="copy_problem_list" name="copy_problem_list" placeholder="逗号分隔,复制后将连续">
+							</div>
+						</div>
+      				</div>
+      				<div class="modal-footer">
+					<button type="submit" class="btn btn-success">确定</button>
+					</form>
+        				<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+      				</div>
+    			</div>
+  		</div>
+	</div>
+
+EOF;
+	}
+
+	if ($print_datum_button) {
+		echo <<<EOF
+	<div class="text-right">
+		<a type="button" class="mt-2 btn btn-secondary" href="/datums">自动生成数据</a>
+	</div>
+EOF;
+	}
+
+	echo "<div class='mt-4'>";
+	echo $pag->pagination();
+	echo "</div>";
+?>
+<?php echoUOJPageFooter() ?>

+ 58 - 0
app/controllers/problem_statement_manage.php

@@ -0,0 +1,58 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
+		become404Page();
+	}
+	if (!hasProblemCtrlPermission($myUser, $problem)) {
+		become403Page();
+	}
+	
+	$problem_content = queryProblemContent($problem['id']);
+	$problem_tags = queryProblemTags($problem['id']);
+	
+	$problem_editor = new UOJBlogEditor();
+	$problem_editor->name = 'problem';
+	$problem_editor->blog_url = "/problem/{$problem['id']}";
+	$problem_editor->cur_data = array(
+		'title' => $problem['title'],
+		'content_md' => $problem_content['statement_md'],
+		'content' => $problem_content['statement'],
+		'tags' => $problem_tags,
+		'is_hidden' => $problem['is_hidden']
+	);
+	$problem_editor->label_text = array_merge($problem_editor->label_text, array(
+		'view blog' => '查看题目',
+		'blog visibility' => '题目可见性'
+	));
+	
+	$problem_editor->save = function($data) {
+		global $problem, $problem_tags;
+		DB::update("update problems set title = '".DB::escape($data['title'])."' where id = {$problem['id']}");
+		DB::update("update problems_contents set statement = '".DB::escape($data['content'])."', statement_md = '".DB::escape($data['content_md'])."' where id = {$problem['id']}");
+		
+		if ($data['tags'] !== $problem_tags) {
+			DB::delete("delete from problems_tags where problem_id = {$problem['id']}");
+			foreach ($data['tags'] as $tag) {
+				DB::insert("insert into problems_tags (problem_id, tag) values ({$problem['id']}, '".DB::escape($tag)."')");
+			}
+		}
+		if ($data['is_hidden'] != $problem['is_hidden'] ) {
+			DB::update("update problems set is_hidden = {$data['is_hidden']} where id = {$problem['id']}");
+			DB::update("update submissions set is_hidden = {$data['is_hidden']} where problem_id = {$problem['id']}");
+			DB::update("update hacks set is_hidden = {$data['is_hidden']} where problem_id = {$problem['id']}");
+		}
+	};
+	
+	$problem_editor->runAtServer();
+?>
+<?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - 编辑 - 题目管理') ?>
+<h1 class="page-header" align="center">#<?=$problem['id']?> : <?=$problem['title']?> 管理</h1>
+<ul class="nav nav-tabs" role="tablist">
+	<li class="nav-item"><a class="nav-link active" href="/problem/<?= $problem['id'] ?>/manage/statement" role="tab">编辑</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/managers" role="tab">管理者</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?= $problem['id'] ?>/manage/data" role="tab">数据</a></li>
+	<li class="nav-item"><a class="nav-link" href="/problem/<?=$problem['id']?>" role="tab">返回</a></li>
+</ul>
+<?php $problem_editor->printHTML() ?>
+<?php echoUOJPageFooter() ?>

+ 169 - 0
app/controllers/problem_statistics.php

@@ -0,0 +1,169 @@
+<?php
+	if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
+		become404Page();
+	}
+	
+	$contest = validateUInt($_GET['contest_id']) ? queryContest($_GET['contest_id']) : null;
+	if ($contest != null) {
+		genMoreContestInfo($contest);
+		if (!isContestProblemVisibleToUser($problem, $contest, $myUser)) {
+			become404Page();
+		}
+	} else {
+		if (!isProblemVisibleToUser($problem, $myUser)) {
+			become404Page();
+		}
+	}
+
+	function scoreDistributionData() {
+		$data = array();
+		$result = DB::select("select score, count(*) from submissions where problem_id = {$_GET['id']} and score is not null group by score");
+		$is_res_empty = true;
+		$has_score_0 = false;
+		$has_score_100 = false;
+		while ($row = DB::fetch($result, MYSQLI_NUM)) {
+			if ($row[0] == 0) {
+				$has_score_0 = true;
+			} elseif ($row[0] == 100) {
+				$has_score_100 = true;
+			}
+			$score = $row[0] * 100;
+			$data[] = array('score' => $score, 'count' => $row[1]);
+		}
+		if (!$has_score_0) {
+			array_unshift($data, array('score' => 0, 'count' => 0));
+		}
+		if (!$has_score_100) {
+			$data[] = array('score' => 10000, 'count' => 0);
+		}
+		return $data;
+	}
+	
+	$data = scoreDistributionData();
+	$pre_data = $data;
+	$suf_data = $data;
+	for ($i = 0; $i < count($data); $i++) {
+		$data[$i]['score'] /= 100;
+	}
+	for ($i = 1; $i < count($data); $i++) {
+		$pre_data[$i]['count'] += $pre_data[$i - 1]['count'];
+	}
+	for ($i = count($data) - 1; $i > 0; $i--) {
+		$suf_data[$i - 1]['count'] += $suf_data[$i]['count'];
+	}
+	
+	$submissions_sort_by_choice = !isset($_COOKIE['submissions-sort-by-code-length']) ? 'time' : 'tot_size';
+?>
+<?php
+	$REQUIRE_LIB['morris'] = "";
+?>
+<?php echoUOJPageHeader(HTML::stripTags($problem['title']) . ' - ' . UOJLocale::get('problems::statistics')) ?>
+
+<h1 class="page-header text-center"><?= $problem['title'] ?> <?= UOJLocale::get('problems::statistics') ?></h1>
+
+<?php if ($contest && !hasContestPermission($myUser, $contest) && $contest['cur_progress'] <= CONTEST_IN_PROGRESS): ?>
+<h2 class="text-center text-muted">比赛尚未结束</h2>
+<?php else: ?>
+<h2 class="text-center"><?= UOJLocale::get('problems::accepted submissions') ?></h2>
+<div class="text-right bot-buffer-sm">
+	<div class="btn-group btn-group-sm">
+		<a href="<?=$SERVER['REQUEST_URI']?>" class="<?=$submissions_sort_by_choice == 'time' ? 'btn btn-info btn-xs active' : 'btn btn-info btn-xs'?>" id="submissions-sort-by-run-time"><?= UOJLocale::get('problems::fastest') ?></a>
+		<a href="<?=$SERVER['REQUEST_URI']?>" class="<?=$submissions_sort_by_choice == 'tot_size' ? 'btn btn-info btn-xs active' : 'btn btn-info btn-xs'?>" id="submissions-sort-by-code-length"><?= UOJLocale::get('problems::shortest') ?></a>
+	</div>
+</div>
+
+<script type="text/javascript">
+	$('#submissions-sort-by-run-time').click(function() {
+		$.cookie('submissions-sort-by-run-time', '');
+		$.removeCookie('submissions-sort-by-code-length');
+	});
+	$('#submissions-sort-by-code-length').click(function() {
+		$.cookie('submissions-sort-by-code-length', '');
+		$.removeCookie('submissions-sort-by-run-time');
+	});
+</script>
+
+<?php if ($submissions_sort_by_choice == 'time'): ?>
+	<?php echoSubmissionsList("best_ac_submissions.submission_id = submissions.id and best_ac_submissions.problem_id = {$problem['id']}", 'order by best_ac_submissions.used_time, best_ac_submissions.used_memory, best_ac_submissions.tot_size', array('judge_time_hidden' => '', 'table_name' => 'best_ac_submissions, submissions'), $myUser); ?>
+<?php else: ?>
+	<?php echoSubmissionsList("best_ac_submissions.shortest_id = submissions.id and best_ac_submissions.problem_id = {$problem['id']}", 'order by best_ac_submissions.shortest_tot_size, best_ac_submissions.shortest_used_time, best_ac_submissions.shortest_used_memory', array('judge_time_hidden' => '', 'table_name' => 'best_ac_submissions, submissions'), $myUser); ?>
+<?php endif ?>
+
+<h2 class="text-center"><?= UOJLocale::get('problems::score distribution') ?></h2>
+<div id="score-distribution-chart" style="height: 250px;"></div>
+<script type="text/javascript">
+new Morris.Bar({
+	element: 'score-distribution-chart',
+	data: <?= json_encode($data) ?>,
+	barColors: function(r, s, type) {
+		return getColOfScore(r.label);
+	},
+	xkey: 'score',
+	ykeys: ['count'],
+	labels: ['number'],
+	hoverCallback: function(index, options, content, row) {
+		var scr = row.score;
+		return '<div class="morris-hover-row-label">' + 'score: ' + scr + '</div>' +
+			'<div class="morris-hover-point">' + '<a href="/submissions?problem_id=' + <?= $problem['id'] ?> + '&amp;min_score=' + scr + '&amp;max_score=' + scr + '">' + 'number: ' + row.count + '</a>' + '</div>';
+	},
+	resize: true
+});
+</script>
+
+<h2 class="text-center"><?= UOJLocale::get('problems::prefix sum of score distribution') ?></h2>
+<div id="score-distribution-chart-pre" style="height: 250px;"></div>
+<script type="text/javascript">
+new Morris.Line({
+	element: 'score-distribution-chart-pre',
+	data: <?= json_encode($pre_data) ?>,
+	xkey: 'score',
+	ykeys: ['count'],
+	labels: ['number'],
+	lineColors: function(row, sidx, type) {
+		if (type == 'line') {
+			return '#0b62a4';
+		}
+		return getColOfScore(row.src.score / 100);
+	},
+	xLabelFormat: function(x) {
+		return (x.getTime() / 100).toString();
+	},
+	hoverCallback: function(index, options, content, row) {
+		var scr = row.score / 100;
+		return '<div class="morris-hover-row-label">' + 'score: &le;' + scr + '</div>' +
+			'<div class="morris-hover-point">' + '<a href="/submissions?problem_id=' + <?= $problem['id'] ?> + '&amp;max_score=' + scr + '">' + 'number: ' + row.count + '</a>' + '</div>';
+	},
+	resize: true
+});
+</script>
+
+<h2 class="text-center"><?= UOJLocale::get('problems::suffix sum of score distribution') ?></h2>
+<div id="score-distribution-chart-suf" style="height: 250px;"></div>
+<script type="text/javascript">
+new Morris.Line({
+	element: 'score-distribution-chart-suf',
+	data: <?= json_encode($suf_data) ?>,
+	xkey: 'score',
+	ykeys: ['count'],
+	labels: ['number'],
+	lineColors: function(row, sidx, type) {
+		if (type == 'line') {
+			return '#0b62a4';
+		}
+		return getColOfScore(row.src.score / 100);
+	},
+	xLabelFormat: function(x) {
+		return (x.getTime() / 100).toString();
+	},
+	hoverCallback: function(index, options, content, row) {
+		var scr = row.score / 100;
+		return '<div class="morris-hover-row-label">' + 'score: &ge;' + scr + '</div>' +
+			'<div class="morris-hover-point">' + '<a href="/submissions?problem_id=' + <?= $problem['id'] ?> + '&amp;min_score=' + scr + '">' + 'number: ' + row.count + '</a>' + '</div>';
+	},
+	resize: true
+});
+</script>
+
+<?php endif ?>
+
+<?php echoUOJPageFooter() ?>

+ 10 - 0
app/controllers/ranklist.php

@@ -0,0 +1,10 @@
+<?php
+	if (isset($_GET['type']) && $_GET['type'] == 'rating') {
+		$config = array('page_len' => 100);
+	} else {
+		become404Page();
+	}
+?>
+<?php echoUOJPageHeader('比赛排行榜') ?>
+<?php echoRanklist($config) ?>
+<?php echoUOJPageFooter() ?>

+ 186 - 0
app/controllers/register.php

@@ -0,0 +1,186 @@
+<?php
+	function handleRegisterPost() {
+		if (!crsf_check()) {
+			return '页面已过期';
+		}
+		if (!isset($_POST['username'])) {
+			return "无效表单";
+		}
+		if (!isset($_POST['password'])) {
+			return "无效表单";
+		}
+		if (!isset($_POST['email'])) {
+			return "无效表单";
+		}
+
+		$username = $_POST['username'];
+		$password = $_POST['password'];
+		$email = $_POST['email'];
+		if (!validateUsername($username)) {
+			return "失败:无效用户名。";
+		}
+		if (queryUser($username)) {
+			return "失败:用户名已存在。";
+		}
+		if (!validatePassword($password)) {
+			return "失败:无效密码。";
+		}
+		if (!validateEmail($email)) {
+			return "失败:无效电子邮箱。";
+		}
+
+		$password = getPasswordToStore($password, $username);
+		
+		$esc_email = DB::escape($email);
+		
+		$svn_pw = uojRandString(10);
+		if (!DB::selectCount("SELECT COUNT(*) FROM user_info")) {
+			DB::query("insert into user_info (username, email, password, svn_password, register_time, usergroup) values ('$username', '$esc_email', '$password', '$svn_pw', now(), 'S')");
+		} else {
+			DB::query("insert into user_info (username, email, password, svn_password, register_time, usergroup) values ('$username', '$esc_email', '$password', '$svn_pw', now(), 'B')");
+		}
+		
+		return "欢迎你!" . $username . ",你已成功注册。";
+	}
+	
+	if (isset($_POST['register'])) {
+		echo handleRegisterPost();
+		die();
+	} elseif (isset($_POST['check_username'])) {
+		$username = $_POST['username'];
+		if (validateUsername($username) && !queryUser($username)) {
+			echo '{"ok" : true}';
+		} else {
+			echo '{"ok" : false}';
+		}
+		die();
+	}
+?>
+<?php
+	$REQUIRE_LIB['md5'] = '';
+	$REQUIRE_LIB['dialog'] = '';
+?>
+<?php echoUOJPageHeader(UOJLocale::get('register')) ?>
+<h2 class="page-header"><?= UOJLocale::get('register') ?></h2>
+<form id="form-register" class="form-horizontal">
+	<div id="div-email" class="form-group">
+		<label for="input-email" class="col-sm-2 control-label"><?= UOJLocale::get('email') ?></label>
+		<div class="col-sm-3">
+			<input type="email" class="form-control" id="input-email" name="email" placeholder="<?= UOJLocale::get('enter your email') ?>" maxlength="50" />
+			<span class="help-block" id="help-email"></span>
+		</div>
+	</div>
+	<div id="div-username" class="form-group">
+		<label for="input-username" class="col-sm-2 control-label"><?= UOJLocale::get('username') ?></label>
+		<div class="col-sm-3">
+			<input type="text" class="form-control" id="input-username" name="username" placeholder="<?= UOJLocale::get('enter your username') ?>" maxlength="20" />
+			<span class="help-block" id="help-username"></span>
+		</div>
+	</div>
+	<div id="div-password" class="form-group">
+		<label for="input-password" class="col-sm-2 control-label"><?= UOJLocale::get('password') ?></label>
+		<div class="col-sm-3">
+			<input type="password" class="form-control" id="input-password" name="password" placeholder="<?= UOJLocale::get('enter your password') ?>" maxlength="20" />
+			<input type="password" class="form-control top-buffer-sm" id="input-confirm_password" placeholder="<?= UOJLocale::get('re-enter your password') ?>" 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"><?= UOJLocale::get('submit') ?></button>
+		</div>
+	</div>
+</form>
+
+<script type="text/javascript">
+function checkUsernameNotInUse() {
+	var ok = false;
+	$.ajax({
+		url : '/register',
+		type : 'POST',
+		dataType : 'json',
+		async : false,
+		
+		data : {
+			check_username : '',
+			username : $('#input-username').val()
+		},
+		success : function(data) {
+			ok = data.ok;
+		},
+		error :	function(XMLHttpRequest, textStatus, errorThrown) {
+			alert(XMLHttpRequest.responseText);
+			ok = false;
+		}
+	});
+	return ok;
+}
+function validateRegisterPost() {
+	var ok = true;
+	ok &= getFormErrorAndShowHelp('email', validateEmail);
+	ok &= getFormErrorAndShowHelp('username', function(str) {
+		var err = validateUsername(str);
+		if (err)
+			return err;
+		if (!checkUsernameNotInUse())
+			return '该用户名已被人使用了。';
+		return '';
+	})
+	ok &= getFormErrorAndShowHelp('password', validateSettingPassword);
+	return ok;
+}
+
+function submitRegisterPost() {
+	if (!validateRegisterPost()) {
+		return;
+	}
+	
+	$.post('/register', {
+		_token : "<?= crsf_token() ?>",
+		register : '',
+		username : $('#input-username').val(),
+		email		: $('#input-email').val(),
+		password : md5($('#input-password').val(), "<?= getPasswordClientSalt() ?>")
+	}, function(msg) {
+		if (/^欢迎你!/.test(msg)) {
+			BootstrapDialog.show({
+				title	 : '注册成功',
+				message : msg,
+				type		: BootstrapDialog.TYPE_SUCCESS,
+				buttons: [{
+					label: '好的',
+					action: function(dialog) {
+						dialog.close();
+					}
+				}],
+				onhidden : function(dialog) {
+					var prevUrl = document.referrer;
+					if (!prevUrl) {
+						prevUrl = '/';
+					};
+					window.location.href = prevUrl;
+				}
+			});
+		} else {
+			BootstrapDialog.show({
+				title	 : '注册失败',
+				message : msg,
+				type		: BootstrapDialog.TYPE_DANGER,
+				buttons: [{
+					label: '好的',
+					action: function(dialog) {
+						dialog.close();
+					}
+				}],
+			});
+		}
+	});
+}
+$(document).ready(function() {
+	$('#form-register').submit(function(e) {
+		submitRegisterPost();
+		return false;
+	});
+});
+</script>
+<?php echoUOJPageFooter() ?>

+ 103 - 0
app/controllers/reset_pw.php

@@ -0,0 +1,103 @@
+<?php
+	if (!isset($_GET['p'])) {
+		become404Page();
+	}
+	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 = queryUser($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());
+	}
+?>
+<?php
+	$REQUIRE_LIB['dialog'] = '';
+	$REQUIRE_LIB['md5'] = '';
+?>
+<?php echoUOJPageHeader('更改密码') ?>
+<h2 class="page-header">更改密码</h2>
+<form id="form-reset" class="form-horizontal">
+	<div id="div-password" class="form-group">
+		<label for="input-password" class="col-sm-2 control-label">新密码</label>
+		<div class="col-sm-3">
+			<input type="password" class="form-control" id="input-password" name="password" placeholder="输入新密码" maxlength="20" />
+			<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>
+</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: [{
+							label: '好的',
+							action: function(dialog) {
+								dialog.close();
+							}
+					}]
+				});
+			}
+		});
+		return false;
+	});
+});
+</script>
+<?php echoUOJPageFooter() ?>

+ 55 - 0
app/controllers/rp.php

@@ -0,0 +1,55 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	$REQUIRE_LIB['mathjax'] = '';
+	$REQUIRE_LIB['hljs'] = '';
+
+	$pid = $_GET['lgpid'];
+	$rp = queryRemoteProblemFull($pid);
+	if ($rp == null)
+		become404Page();
+
+	function handleUpload($zip_file_name, $content, $tot_size) {
+		global $rp, $myUser;
+		$content['config'][] = array('problem_id', 260328);
+		$content['config'][] = array('rpid', $rp['pid']);
+		$esc_content = DB::escape(json_encode($content));
+		$esc_language = DB::escape('Remote judger');
+		$result = array();
+		$result['status'] = 'Waiting remote';
+		$result_json = json_encode($result);
+		DB::insert("insert into submissions (problem_id, submit_time, submitter, content, language, tot_size, status, result, is_hidden) values (260328, now(), '${myUser['username']}', '$esc_content', '$esc_language', $tot_size, '${result['status']}', '$result_json', false)");
+	}
+
+	# vfk 把所有的 Available 拼成了 Avaiable。我无语,懒得改了。
+	$submission_requirement = json_decode('[{"name":"answer","type":"source code","file_name":"answer.code", "languages":["C++"]}]', true);
+	$answer_form = newSubmissionForm('answer', $submission_requirement, 'uojRandAvaiableSubmissionFileName', 'handleUpload');
+	$answer_form->succ_href = '/submissions';
+	$answer_form->runAtServer();
+?>
+<?php echoUOJPageHeader(HTML::stripTags($rp['title']) . ' - ' . UOJLocale::get('problems::problem')) ?>
+<div class="row d-flex justify-content-center">
+	<span class="badge badge-secondary mr-1">时间限制:<?=$time_limit!=null?"$time_limit s":"N/A"?></span>
+	<span class="badge badge-secondary mr-1">空间限制:<?=$memory_limit!=null?"$memory_limit MB":"N/A"?></span>
+</div>
+
+<h1 class="page-header text-center"><?= $rp['title'] ?></h1>
+<ul class="nav nav-tabs" role="tablist">
+	<li class="nav-item"><a class="nav-link active" href="#tab-statement" role="tab" data-toggle="tab"><span class="glyphicon glyphicon-book"></span> <?= UOJLocale::get('problems::statement') ?></a></li>
+	<li class="nav-item"><a class="nav-link" href="#tab-submit-answer" role="tab" data-toggle="tab"><span class="glyphicon glyphicon-upload"></span> <?= UOJLocale::get('problems::submit') ?></a></li>
+</ul>
+<div class="tab-content">
+	<div class="tab-pane active" id="tab-statement">
+		<article class="top-buffer-md"><?= $rp['statement'] ?></article>
+	</div>
+	<div class="tab-pane" id="tab-submit-answer">
+		<div class="top-buffer-sm"></div>
+		<?php 
+			if ($myUser['luogu_uid'] != '' && $myUser['luogu_clientid'] != '')
+				echo $answer_form->printHTML();
+			else
+				echo '你需要先在 勋章管理 -> Luogu 账号管理 中绑定自己的账号';
+		?>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 62 - 0
app/controllers/rps.php

@@ -0,0 +1,62 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	requirePHPLib('data');
+	
+	function echoRemoteProblem($problem) {
+		global $myUser;
+		echo '<tr class="text-center">';
+		echo '<td>' . $problem['pid'] . '</td>';
+		echo '<td class="text-left">';
+		echo '<a href="/rp/', $problem['pid'], '">', $problem['title'], '</a>';
+		echo '</td></tr>';
+	}
+	
+	
+	$header = '<tr>';
+	$header .= '<th class="text-center" style="width:5em;">PID</th>';
+	$header .= '<th>远端题目</th>';
+	$header .= '</tr>';
+
+	$pag_config = array('page_len' => 100);
+	$pag_config['col_names'] = array('pid', 'title');
+	$pag_config['table_name'] = "rp";
+	$pag_config['cond'] = '1';
+	$pag_config['tail'] = "order by pid asc";
+	$pag = new Paginator($pag_config);
+
+	$div_classes = array('table-responsive', 'shadow');
+	$table_classes = array('table', 'table-bordered', 'table-hover', 'table-striped');
+?>
+<?php echoUOJPageHeader('远端题目') ?>
+<div class="row my-4">
+	<div class="col-sm-9">
+		<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?>
+	</div>
+</div>
+<div class="top-buffer-sm"></div>
+<?php
+	echo '<div class="', join($div_classes, ' '), '">';
+	echo '<table class="', join($table_classes, ' '), '">';
+	echo '<thead>';
+	echo $header;
+	echo '</thead>';
+	echo '<tbody>';
+
+	foreach ($pag->get() as $idx => $row) {
+		echoRemoteProblem($row);
+		echo "\n";
+	}
+	if ($pag->isEmpty()) {
+		echo '<tr><td class="text-center" colspan="233">'.UOJLocale::get('none').'</td></tr>';
+	}
+	
+	echo '</tbody>';
+	echo '</table>';
+	echo '</div>';
+
+	echo "<div class='mt-4'>";
+	echo $pag->pagination();
+	echo "</div>";
+?>
+<?php echoUOJPageFooter() ?>

BIN
app/controllers/subdomain/blog/.archive.php.swp


+ 15 - 0
app/controllers/subdomain/blog/aboutme.php

@@ -0,0 +1,15 @@
+<?php requirePHPLib('form') ?>
+<?php echoUOJPageHeader('关于我') ?>
+
+<?php
+	$user = UOJContext::user();
+	$blogid = $user['aboutme_blogid'];
+	if (!$blogid) {
+		echo '<article><p>博主暂未设置自定义页面</p><em>(可以到“勋章管理”中设置自定义页面)</em></article>';
+	}
+	else {
+		echoBlog(queryBlog($blogid), array('show_content_only' => true));
+	}
+?>
+
+<?php echoUOJPageFooter() ?>

+ 164 - 0
app/controllers/subdomain/blog/archive.php

@@ -0,0 +1,164 @@
+<?php
+	requirePHPLib('form');
+
+	if (!UOJContext::userid())
+		$blogs_cond = "is_draft = false";
+	else
+		$blogs_cond = "poster = '".UOJContext::userid()."' and is_draft = false";
+	if (!UOJContext::hasBlogPermission()) {
+		$blogs_cond .= " and is_hidden = false";
+	}
+	
+	$display_blogs_cond = $blogs_cond;
+	
+	if (isset($_GET['tag'])) {
+		$blog_tag_required = $_GET['tag'];
+		$display_blogs_cond .= " and '".DB::escape($blog_tag_required)."' in (select tag from blogs_tags where blogs_tags.blog_id = blogs.id)";
+	} else {
+		$blog_tag_required = null;
+	}
+	
+	$blogs_pag = new Paginator(array(
+		'col_names' => array('*'),
+		'table_name' => 'blogs',
+		'cond' => $display_blogs_cond,
+		'tail' => 'order by post_time desc',
+		'page_len' => 10
+	));
+	
+	$all_tags = DB::selectAll("select distinct tag from blogs_tags where blog_id in (select id from blogs where $blogs_cond)");
+	
+	requireLib('mathjax');
+	requireLib('hljs');
+?>
+<?php echoUOJPageHeader('日志') ?>
+
+<div class="row">
+	<div class="col-md-3">
+		<?php if (UOJContext::hasBlogPermission()): ?>
+		<div class="btn-group d-flex">
+			<a href="<?=HTML::blog_url(UOJContext::userid(), '/post/new/write')?>" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> 写新博客</a>
+			<a href="<?=HTML::blog_url(UOJContext::userid(), '/slide/new/write')?>" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> 写新幻灯片</a>
+		</div>
+		<?php endif ?>
+		<div class="card border-info top-buffer-sm">
+			<div class="card-header bg-info">标签</div>
+			<div class="card-body">
+			<?php if ($all_tags): ?>
+			<?php foreach ($all_tags as $tag): ?>
+				<?php echoBlogTag($tag['tag']) ?>
+			<?php endforeach ?>
+			<?php else: ?>
+				<div class="text-muted">暂无</div>
+			<?php endif ?>
+			</div>
+		</div>
+	</div>
+	<div class="col-md-9 blog-list">
+		<?php if (!$blog_tag_required): ?>
+			<?php if ($blogs_pag->isEmpty()): ?>
+			<div class="text-muted">此人很懒,什么博客也没留下。</div>
+			<?php else: ?>
+			<?php foreach ($blogs_pag->get() as $blog): ?>
+				<?php echoBlog($blog, array('is_preview' => true)) ?>
+			<?php endforeach ?>
+<div class="text-right text-muted" style="display: none;">共 <?= $blogs_pag->n_rows ?> 篇博客</div>
+			<?php endif ?>
+		<?php else: ?>
+			<?php if ($blogs_pag->isEmpty()): ?>
+			<div class="alert alert-danger">
+				没有找到包含 “<?= HTML::escape($blog_tag_required) ?>” 标签的博客:
+			</div>
+			<?php else: ?>
+			<div class="alert alert-success" style="">
+				共找到 <?= $blogs_pag->n_rows ?> 篇包含 “<?= HTML::escape($blog_tag_required) ?>” 标签的博客:
+			</div>
+			<?php foreach ($blogs_pag->get() as $blog): ?>
+				<?php echoBlog($blog, array('is_preview' => true)) ?>
+			<?php endforeach ?>
+			<?php endif ?>
+		<?php endif ?>
+	</div>
+	<script>
+		(function () {
+			let currentPage = 1, isLoading = false, hasMore = true;
+			const blogList = document.querySelector('.blog-list');
+			const itemSelector = '.blog-item';
+			const tag = (new URLSearchParams(window.location.search)).get('tag');
+
+			if (blogList.querySelectorAll('.blog-item').length < 10)
+				hasMore = false;
+
+			function extractSummary(html) {
+				const div = document.createElement('div');
+				div.innerHTML = html;
+				let text = div.textContent || div.innerText || '';
+				text = text.replace(/\s+/g, ' ').trim();
+				return text;
+			}
+
+			function collapseArticle(post) {
+				const content = post.querySelector('.blog-content');
+				const divorig = post.querySelector('.blog-content-orig');
+				const divsummary = post.querySelector('.blog-content-summary');
+
+				let summary = extractSummary(divorig.innerHTML);
+				const maxLen = 150;
+
+				if (summary.length < maxLen) return;
+				summary = summary.slice(0, maxLen) + '……';
+				divsummary.innerHTML = summary;
+
+				divsummary.style.display = 'block';
+				divorig.style.display = 'none';
+				content.classList.add('collapsed');
+
+				content.addEventListener('click', () => {
+					divsummary.style.display = 'none';
+					divorig.style.display = 'block';
+					content.classList.remove('collapsed');
+					content.classList.add('expand');
+				});
+			}
+
+			document.querySelectorAll('.blog-item').forEach(collapseArticle);
+
+			async function loadPage(page) {
+				if (isLoading || !hasMore) return;
+				isLoading = true;
+
+				const url = !tag ? `/?page=${page}` : `/?page=${page}&tag=${tag}`;
+				try {
+					const resp = await fetch(url);
+					const html = await resp.text();
+					const parser = new DOMParser();
+					const doc = parser.parseFromString(html, 'text/html');
+					const items = doc.querySelectorAll(itemSelector);
+					if (items.length == 0) {
+						hasMore = false;
+						return;
+					}
+					items.forEach(item => { blogList.appendChild(item); collapseArticle(item); });
+					if (items.length < 10)
+						hasMore = false;
+
+					MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
+					hljs.highlightAll();
+					$('body').uoj_highlight();
+				} catch (err) {
+					console.log('加载失败', err);
+				} finally {
+					isLoading = false;		
+				}
+			}
+			window.addEventListener('scroll', () => {
+				const scrollTop = document.documentElement.scrollTop;
+				const windowHeight = window.innerHeight;
+				const docHeight = document.documentElement.scrollHeight;
+				if (scrollTop + windowHeight >= docHeight - 200 && !isLoading && hasMore)
+					loadPage(++currentPage);
+			});
+		})();
+	</script>
+</div>
+<?php echoUOJPageFooter() ?>

+ 196 - 0
app/controllers/subdomain/blog/blog.php

@@ -0,0 +1,196 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!isset($_GET['id']) || !validateUInt($_GET['id']) || !($blog = queryBlog($_GET['id'])) || !UOJContext::isHis($blog)) {
+		become404Page();
+	}
+	if ($blog['is_hidden'] && !UOJContext::hasBlogPermission()) {
+		become403Page();
+	}
+	
+	$comment_form = new UOJForm('comment');
+	$comment_form->addVTextArea('comment', '内容', '',
+		function($comment) {
+			global $myUser;
+			if ($myUser == null) {
+				return '请先登录';
+			}
+			if (!$comment) {
+				return '评论不能为空';
+			}
+			if (strlen($comment) > 1000) {
+				return '不能超过1000个字节';
+			}
+			return '';
+		},
+		null
+	);
+	$comment_form->handle = function() {
+		global $myUser, $blog, $comment_form;
+		$comment = HTML::escape($_POST['comment']);
+		
+		list($comment, $referrers) = uojHandleAtSign($comment, "/post/{$blog['id']}");
+		
+		$esc_comment = DB::escape($comment);
+		DB::insert("insert into blogs_comments (poster, blog_id, content, reply_id, post_time, zan) values ('{$myUser['username']}', '{$blog['id']}', '$esc_comment', 0, now(), 0)");
+		$comment_id = DB::insert_id();
+		
+		$rank = DB::selectCount("select count(*) from blogs_comments where blog_id = {$blog['id']} and reply_id = 0 and id < {$comment_id}");
+		$page = floor($rank / 20) + 1;
+		
+		$uri = getLongTablePageUri($page) . '#' . "comment-{$comment_id}";
+		
+		foreach ($referrers as $referrer) {
+			$content = '有人在博客 ' . $blog['title'] . ' 的评论里提到你:<a href="' . $uri . '">点击此处查看</a>';
+			sendSystemMsg($referrer, '有人提到你', $content);
+		}
+		
+		if ($blog['poster'] !== $myUser['username']) {
+			$content = '有人回复了您的博客 ' . $blog['title'] . ' :<a href="' . $uri . '">点击此处查看</a>';
+			sendSystemMsg($blog['poster'], '博客新回复通知', $content);
+		}
+		
+		$comment_form->succ_href = getLongTablePageRawUri($page);
+	};
+	$comment_form->ctrl_enter_submit = true;
+	
+	$comment_form->runAtServer();
+	
+	$reply_form = new UOJForm('reply');
+	$reply_form->addHidden('reply_id', '0',
+		function($reply_id, &$vdata) {
+			global $blog;
+			if (!validateUInt($reply_id) || $reply_id == 0) {
+				return '您要回复的对象不存在';
+			}
+			$comment = queryBlogComment($reply_id);
+			if (!$comment || $comment['blog_id'] != $blog['id']) {
+				return '您要回复的对象不存在';
+			}
+			$vdata['parent'] = $comment;
+			return '';
+		},
+		null
+	);
+	$reply_form->addVTextArea('reply_comment', '内容', '',
+		function($comment) {
+			global $myUser;
+			if ($myUser == null) {
+				return '请先登录';
+			}
+			if (!$comment) {
+				return '评论不能为空';
+			}
+			if (strlen($comment) > 140) {
+				return '不能超过140个字节';
+			}
+			return '';
+		},
+		null
+	);
+	$reply_form->handle = function(&$vdata) {
+		global $myUser, $blog, $reply_form;
+		$comment = HTML::escape($_POST['reply_comment']);
+		
+		list($comment, $referrers) = uojHandleAtSign($comment, "/post/{$blog['id']}");
+		
+		$reply_id = $_POST['reply_id'];
+		
+		$esc_comment = DB::escape($comment);
+		DB::insert("insert into blogs_comments (poster, blog_id, content, reply_id, post_time, zan) values ('{$myUser['username']}', '{$blog['id']}', '$esc_comment', $reply_id, now(), 0)");
+		$comment_id = DB::insert_id();
+		
+		$rank = DB::selectCount("select count(*) from blogs_comments where blog_id = {$blog['id']} and reply_id = 0 and id < {$reply_id}");
+		$page = floor($rank / 20) + 1;
+		
+		$uri = getLongTablePageUri($page) . '#' . "comment-{$reply_id}";
+		
+		foreach ($referrers as $referrer) {
+			$content = '有人在博客 ' . $blog['title'] . ' 的评论里提到你:<a href="' . $uri . '">点击此处查看</a>';
+			sendSystemMsg($referrer, '有人提到你', $content);
+		}
+		
+		$parent = $vdata['parent'];
+		$notified = array();
+		if ($parent['poster'] !== $myUser['username']) {
+			$notified[] = $parent['poster'];
+			$content = '有人回复了您在博客 ' . $blog['title'] . ' 下的评论 :<a href="' . $uri . '">点击此处查看</a>';
+			sendSystemMsg($parent['poster'], '评论新回复通知', $content);
+		}
+		if ($blog['poster'] !== $myUser['username'] && !in_array($blog['poster'], $notified)) {
+			$notified[] = $blog['poster'];
+			$content = '有人回复了您的博客 ' . $blog['title'] . ' :<a href="' . $uri . '">点击此处查看</a>';
+			sendSystemMsg($blog['poster'], '博客新回复通知', $content);
+		}
+		
+		$reply_form->succ_href = getLongTablePageRawUri($page);
+	};
+	$reply_form->ctrl_enter_submit = true;
+	
+	$reply_form->runAtServer();
+	
+	$comments_pag = new Paginator(array(
+		'col_names' => array('*'),
+		'table_name' => 'blogs_comments',
+		'cond' => 'blog_id = ' . $blog['id'] . ' and reply_id = 0',
+		'tail' => 'order by id asc',
+		'page_len' => 20
+	));
+?>
+<?php
+	$REQUIRE_LIB['mathjax'] = '';
+	$REQUIRE_LIB['hljs'] = '';
+?>
+<?php echoUOJPageHeader(HTML::stripTags($blog['title']) . ' - 博客') ?>
+<?php echoBlog($blog, array('show_title_only' => isset($_GET['page']) && $_GET['page'] != 1)) ?>
+<h2>评论 <span class="glyphicon glyphicon-comment"></span></h2>
+<div class="list-group">
+<?php if ($comments_pag->isEmpty()): ?>
+	<div class="list-group-item text-muted">暂无评论</div>
+<?php else: ?>
+	<?php foreach ($comments_pag->get() as $comment):
+		$poster = queryUser($comment['poster']);
+		$esc_email = HTML::escape($poster['email']);
+		$asrc = HTML::avatar_addr($poster, 80);
+		
+		$replies = DB::selectAll("select id, poster, content, post_time from blogs_comments where reply_id = {$comment['id']} order by id");
+		foreach ($replies as $idx => $reply) {
+			$replies[$idx]['poster_rating'] = queryUser($reply['poster'])['rating'];
+		}
+		$replies_json = json_encode($replies);
+	?>
+	<div id="comment-<?= $comment['id'] ?>" class="list-group-item">
+		<div class="media">
+			<div class="media-left comtposterbox mr-3">
+				<a href="<?= HTML::url('/user/profile/'.$poster['username']) ?>" class="d-none d-sm-block">
+					<img class="media-object img-rounded" src="<?= $asrc ?>" alt="avatar" />
+				</a>
+			</div>
+			<div id="comment-body-<?= $comment['id'] ?>" class="media-body comtbox">
+				<div class="row">
+					<div class="col-sm-6"><?= getUserLink($poster['username']) ?></div>
+					<div class="col-sm-6 text-right"><?= getClickZanBlock('BC', $comment['id'], $comment['zan']) ?></div>
+				</div>
+				<div class="comtbox1"><?= $comment['content'] ?></div>
+				<ul class="text-right list-inline bot-buffer-no"><li><small class="text-muted"><?= $comment['post_time'] ?></small></li><li><a id="reply-to-<?= $comment['id'] ?>" href="#">回复</a></li></ul>
+				<?php if ($replies): ?>
+				<div id="replies-<?= $comment['id'] ?>" class="comtbox5"></div>
+				<?php endif ?>
+				<script type="text/javascript">showCommentReplies('<?= $comment['id'] ?>', <?= $replies_json ?>);</script>
+			</div>
+		</div>
+	</div>
+	<?php endforeach ?>
+<?php endif ?>
+</div>
+<?= $comments_pag->pagination() ?>
+
+<h3 class="mt-4">发表评论</h3>
+<p>可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。</p>
+<?php $comment_form->printHTML() ?>
+
+<div id="div-form-reply" style="display:none">
+	<?php $reply_form->printHTML() ?>
+</div>
+
+<?php echoUOJPageFooter() ?>

+ 25 - 0
app/controllers/subdomain/blog/blog_delete.php

@@ -0,0 +1,25 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!UOJContext::hasBlogPermission()) {
+		become403Page();
+	}
+	if (!isset($_GET['id']) || !validateUInt($_GET['id']) || !($blog = queryBlog($_GET['id'])) || !UOJContext::isHis($blog)) {
+		become404Page();
+	}
+	
+	$delete_form = new UOJForm('delete');
+	$delete_form->handle = function() {
+		global $blog;
+		deleteBlog($blog['id']);
+	};
+	$delete_form->submit_button_config['class_str'] = 'btn btn-danger';
+	$delete_form->submit_button_config['text'] = '是的,我确定要删除';
+	$delete_form->succ_href = "/archive";
+	
+	$delete_form->runAtServer();
+?>
+<?php echoUOJPageHeader('删除博客 - ' . HTML::stripTags($blog['title'])) ?>
+<h3>您真的要删除博客 <?= $blog['title'] ?> 吗?该操作不可逆!</h3>
+<?php $delete_form->printHTML(); ?>
+<?php echoUOJPageFooter() ?>

+ 88 - 0
app/controllers/subdomain/blog/blog_write.php

@@ -0,0 +1,88 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!UOJContext::hasBlogPermission()) {
+		become403Page();
+	}
+	if (isset($_GET['id'])) {
+		if (!validateUInt($_GET['id']) || !($blog = queryBlog($_GET['id'])) || !UOJContext::isHisBlog($blog)) {
+			become404Page();
+		}
+	} else {
+		$blog = DB::selectFirst("select * from blogs where poster = '".UOJContext::user()['username']."' and type = 'B' and is_draft = true");
+	}
+	
+	$blog_editor = new UOJBlogEditor();
+	$blog_editor->name = 'blog';
+	if ($blog) {
+		$blog_editor->cur_data = array(
+			'title' => $blog['title'],
+			'content_md' => $blog['content_md'],
+			'content' => $blog['content'],
+			'tags' => queryBlogTags($blog['id']),
+			'is_hidden' => $blog['is_hidden']
+		);
+	} else {
+		$blog_editor->cur_data = array(
+			'title' => '新博客',
+			'content_md' => '',
+			'content' => '',
+			'tags' => array(),
+			'is_hidden' => true
+		);
+	}
+	if ($blog && !$blog['is_draft']) {
+		$blog_editor->blog_url = HTML::blog_url(UOJContext::user()['username'], "/post/{$blog['id']}");
+	} else {
+		$blog_editor->blog_url = null;
+	}
+	
+	function updateBlog($id, $data) {
+		DB::update("update blogs set title = '".DB::escape($data['title'])."', content = '".DB::escape($data['content'])."', content_md = '".DB::escape($data['content_md'])."', is_hidden = {$data['is_hidden']} where id = {$id}");
+	}
+	function insertBlog($data) {
+		DB::insert("insert into blogs (title, content, content_md, poster, is_hidden, is_draft, post_time) values ('".DB::escape($data['title'])."', '".DB::escape($data['content'])."', '".DB::escape($data['content_md'])."', '".Auth::id()."', {$data['is_hidden']}, {$data['is_draft']}, now())");
+	}
+	
+	$blog_editor->save = function($data) {
+		global $blog;
+		$ret = array();
+		if ($blog) {
+			if ($blog['is_draft']) {
+				if ($data['is_hidden']) {
+					updateBlog($blog['id'], $data);
+				} else {
+					deleteBlog($blog['id']);
+					insertBlog(array_merge($data, array('is_draft' => 0)));
+					$blog = array('id' => DB::insert_id(), 'tags' => array());
+					$ret['blog_write_url'] = HTML::blog_url(UOJContext::user()['username'], "/post/{$blog['id']}/write");
+					$ret['blog_url'] = HTML::blog_url(UOJContext::user()['username'], "/post/{$blog['id']}");
+				}
+			} else {
+				updateBlog($blog['id'], $data);
+			}
+		} else {
+			insertBlog(array_merge($data, array('is_draft' => $data['is_hidden'] ? 1 : 0)));
+			$blog = array('id' => DB::insert_id(), 'tags' => array());
+			if (!$data['is_hidden']) {
+				$ret['blog_write_url'] = HTML::blog_url(UOJContext::user()['username'], "/post/{$blog['id']}/write");
+				$ret['blog_url'] = HTML::blog_url(UOJContext::user()['username'], "/post/{$blog['id']}");
+			}
+		}
+		if ($data['tags'] !== $blog['tags']) {
+			DB::delete("delete from blogs_tags where blog_id = {$blog['id']}");
+			foreach ($data['tags'] as $tag) {
+				DB::insert("insert into blogs_tags (blog_id, tag) values ({$blog['id']}, '".DB::escape($tag)."')");
+			}
+		}
+		return $ret;
+	};
+	
+	$blog_editor->runAtServer();
+?>
+<?php echoUOJPageHeader('写博客') ?>
+<div class="text-right">
+<a href="http://uoj.ac/blog/7">这玩意儿怎么用?</a>
+</div>
+<?php $blog_editor->printHTML() ?>
+<?php echoUOJPageFooter() ?>

+ 31 - 0
app/controllers/subdomain/blog/index.php

@@ -0,0 +1,31 @@
+<?php
+	$blogs_pag = new Paginator(array(
+		'col_names' => array('*'),
+		'table_name' => 'blogs',
+		'cond' => "poster = '".UOJContext::user()['username']."' and is_hidden = 0",
+		'tail' => 'order by post_time desc, id desc',
+	));
+
+?>
+<?php
+	$REQUIRE_LIB['mathjax'] = '';
+	$REQUIRE_LIB['hljs'] = '';
+?>
+<?php echoUOJPageHeader(UOJContext::user()['username'] . '的博客') ?>
+
+<div class="row">
+	<div class="col-md-9">
+		<?php if ($blogs_pag->isEmpty()): ?>
+		<div class="text-muted">此人很懒,什么博客也没留下。</div>
+		<?php else: ?>
+		<?php foreach ($blogs_pag->get() as $blog): ?>
+			<?php echo echoBlogCard($blog) ?>
+		<?php endforeach ?>
+		<?php endif ?>
+		<?php echo $blogs_pag->pagination() ?>
+	</div>
+	<div class="col-md-3">
+		<img class="media-object img-thumbnail center-block" alt="<?= UOJContext::user()['username'] ?> Avatar" src="<?= HTML::avatar_addr(UOJContext::user(), 256) ?>" />
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 51 - 0
app/controllers/subdomain/blog/route.php

@@ -0,0 +1,51 @@
+<?php
+
+call_user_func(function() { // to prevent variable scope leak
+
+	Route::pattern('id', '[1-9][0-9]{0,9}');
+	Route::pattern('blog_username', '[a-zA-Z0-9_\-]{1,20}');
+
+	switch (UOJConfig::$data['switch']['blog-domain-mode']) {
+		case 1:
+			$domain = '{blog_username}.'.UOJConfig::$data['web']['main']['host'];
+			$prefix = '';
+			break;
+#		case 2:
+#			$domain = UOJConfig::$data['web']['blog']['host'];
+#			$prefix = '/{blog_username}';
+#			break;
+#		case 3:
+#			$domain = UOJConfig::$data['web']['main']['host'];
+#			$prefix = '/blog/{blog_username}';
+#			break;
+	}
+
+	Route::group([
+			'domain' => UOJConfig::$data['web']['blog']['host']
+		], function() {
+			Route::any("/", '/blogs.php');
+			Route::any("/blogs/{id}", '/blog_show.php');
+			Route::any("/post/{id}", '/blog_show.php');
+		}
+	);
+
+	Route::group([
+			'domain' => $domain,
+			'onload' => function() {
+				UOJContext::setupBlog();
+			}
+		], function() use ($prefix) {
+			Route::any("$prefix/", '/subdomain/blog/archive.php');
+			Route::any("$prefix/archive", '/subdomain/blog/archive.php');
+			Route::any("$prefix/aboutme", '/subdomain/blog/aboutme.php');
+			Route::any("$prefix/click-zan", '/click_zan.php');
+			Route::any("$prefix/blogs/{id}", '/blog_show.php');
+			Route::any("$prefix/post/{id}", '/subdomain/blog/blog.php');
+			Route::any("$prefix/slide/{id}", '/subdomain/blog/slide.php');
+			Route::any("$prefix/post/(?:{id}|new)/write", '/subdomain/blog/blog_write.php');
+			Route::any("$prefix/slide/(?:{id}|new)/write", '/subdomain/blog/slide_write.php');
+			Route::any("$prefix/post/{id}/delete", '/subdomain/blog/blog_delete.php');
+		}
+	);
+
+});

+ 15 - 0
app/controllers/subdomain/blog/slide.php

@@ -0,0 +1,15 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!isset($_GET['id']) || !validateUInt($_GET['id']) || !($blog = queryBlog($_GET['id'])) || !UOJContext::isHisSlide($blog)) {
+		become404Page();
+	}
+	if ($blog['is_hidden'] && !UOJContext::hasBlogPermission()) {
+		become403Page();
+	}
+	
+	$page_config = UOJContext::pageConfig();
+	$page_config['PageTitle'] = HTML::stripTags($blog['title']) . ' - 幻灯片';
+	$page_config['content'] = $blog['content'];
+	uojIncludeView('slide', $page_config);
+?>

+ 85 - 0
app/controllers/subdomain/blog/slide_write.php

@@ -0,0 +1,85 @@
+<?php
+	requirePHPLib('form');
+	
+	if (!UOJContext::hasBlogPermission()) {
+		become403Page();
+	}
+	if (isset($_GET['id'])) {
+		if (!validateUInt($_GET['id']) || !($blog = queryBlog($_GET['id'])) || !UOJContext::isHisSlide($blog)) {
+			become404Page();
+		}
+	} else {
+		$blog = DB::selectFirst("select * from blogs where poster = '".UOJContext::user()['username']."' and type = 'S' and is_draft = true");
+	}
+	
+	$blog_editor = new UOJBlogEditor();
+	$blog_editor->type = 'slide';
+	$blog_editor->name = 'blog';
+	if ($blog) {
+		$blog_editor->cur_data = array(
+			'title' => $blog['title'],
+			'content_md' => $blog['content_md'],
+			'content' => $blog['content'],
+			'tags' => queryBlogTags($blog['id']),
+			'is_hidden' => $blog['is_hidden']
+		);
+	} else {
+		$blog_editor->cur_data = array(
+			'title' => '新幻灯片',
+			'content_md' => '',
+			'content' => '',
+			'tags' => array(),
+			'is_hidden' => true
+		);
+	}
+	if ($blog && !$blog['is_draft']) {
+		$blog_editor->blog_url = HTML::blog_url(UOJContext::user()['username'], "/post/{$blog['id']}");
+	} else {
+		$blog_editor->blog_url = null;
+	}
+	
+	function updateBlog($id, $data) {
+		DB::update("update blogs set title = '".DB::escape($data['title'])."', content = '".DB::escape($data['content'])."', content_md = '".DB::escape($data['content_md'])."', is_hidden = {$data['is_hidden']} where id = {$id}");
+	}
+	function insertSlide($data) {
+		DB::insert("insert into blogs (type, title, content, content_md, poster, is_hidden, is_draft, post_time) values ('S', '".DB::escape($data['title'])."', '".DB::escape($data['content'])."', '".DB::escape($data['content_md'])."', '".Auth::id()."', {$data['is_hidden']}, {$data['is_draft']}, now())");
+	}
+	
+	$blog_editor->save = function($data) {
+		global $blog;
+		$ret = array();
+		if ($blog) {
+			if ($blog['is_draft']) {
+				if ($data['is_hidden']) {
+					updateBlog($blog['id'], $data);
+				} else {
+					deleteBlog($blog['id']);
+					insertSlide(array_merge($data, array('is_draft' => 0)));
+					$blog = array('id' => DB::insert_id(), 'tags' => array());
+					$ret['blog_write_url'] = HTML::blog_url(UOJContext::user()['username'], "/slide/{$blog['id']}/write");
+					$ret['blog_url'] = HTML::blog_url(UOJContext::user()['username'], "/post/{$blog['id']}");
+				}
+			} else {
+				updateBlog($blog['id'], $data);
+			}
+		} else {
+			insertSlide(array_merge($data, array('is_draft' => $data['is_hidden'] ? 1 : 0)));
+			$blog = array('id' => DB::insert_id(), 'tags' => array());
+		}
+		if ($data['tags'] !== $blog['tags']) {
+			DB::delete("delete from blogs_tags where blog_id = {$blog['id']}");
+			foreach ($data['tags'] as $tag) {
+				DB::insert("insert into blogs_tags (blog_id, tag) values ({$blog['id']}, '".DB::escape($tag)."')");
+			}
+		}
+		return $ret;
+	};
+	
+	$blog_editor->runAtServer();
+?>
+<?php echoUOJPageHeader('写幻灯片') ?>
+<div class="text-right">
+<a href="http://uoj.ac/blog/75">这玩意儿怎么用?</a>
+</div>
+<?php $blog_editor->printHTML() ?>
+<?php echoUOJPageFooter() ?>

+ 238 - 0
app/controllers/submission.php

@@ -0,0 +1,238 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	
+	if (!validateUInt($_GET['id']) || !($submission = querySubmission($_GET['id']))) {
+		become404Page();
+	}
+	$submission_result = json_decode($submission['result'], true);
+
+	$problem = queryProblemBrief($submission['problem_id']);
+	$problem_extra_config = getProblemExtraConfig($problem);
+	
+	if ($submission['contest_id']) {
+		$contest = queryContest($submission['contest_id']);
+		genMoreContestInfo($contest);
+	} else {
+		$contest = null;
+	}
+	if (!isSubmissionVisibleToUser($submission, $problem, $myUser)) {
+		become403Page();
+	}
+	
+	if ($submission['status'] == 'Judged' && $submission['result_error'] == 'Need Captcha') {
+		if ($submission['submitter'] == $myUser['username'] || isSuperUser($myUser)) {
+			foreach (json_decode($submission['content'], true)['config'] as $k)
+				if ($k[0] == 'captcha') 
+					$captcha = $k[1];
+
+			$captcha_form = new UOJForm('rmj_captcha');
+			$captcha_form->addInput('captcha_answer', 'text', '验证码', '', 
+				function ($x) {
+					if (strlen($x) != 4)
+						return '长度错误';
+					return '';
+				}, null);
+			$captcha_form->handle = function() {
+				global $submission;
+
+				$content = json_decode($submission['content'], true);
+				$capt = DB::escape($_POST['captcha_answer']);
+				$content['config'][] = array('captcha_answer', $capt);
+				$content = DB::escape(json_encode($content));
+				$sid = $submission['id'];
+				DB::update("update submissions set content = '{$content}' where id = {$sid}");
+				DB::update("update submissions set status = 'Waiting remote' where id = {$sid}");
+			};
+			$captcha_form->runAtServer();
+			$should_show_captcha = true;
+
+			echo '<div id="rmj-captcha" data-captcha="' . $captcha . '"></div>';
+		}
+	}
+
+	$out_status = explode(', ', $submission['status'])[0];
+	
+	if ($_GET['get'] == 'status-details' && Auth::check() && $submission['submitter'] === Auth::id()) {
+		echo json_encode(array(
+			'judged' => $out_status == 'Judged',
+			'html' => getSubmissionStatusDetails($submission)
+		));
+		die();
+	}
+	
+	$hackable = $submission['score'] == 100 && $problem['hackable'] == 1;
+	if ($hackable) {
+		$hack_form = new UOJForm('hack');	
+		
+		$hack_form->addTextFileInput('input', '输入数据');
+		$hack_form->addCheckBox('use_formatter', '帮我整理文末回车、行末空格、换行符', true);
+		$hack_form->handle = function(&$vdata) {
+			global $myUser, $problem, $submission;
+			if ($myUser == null) {
+				redirectToLogin();
+			}
+			
+			if ($_POST["input_upload_type"] == 'file') {
+				$tmp_name = UOJForm::uploadedFileTmpName("input_file");
+				if ($tmp_name == null) {
+					becomeMsgPage('你在干啥……怎么什么都没交过来……?');
+				}
+			}
+			
+			$fileName = uojRandAvaiableTmpFileName();
+			$fileFullName = UOJContext::storagePath().$fileName;
+			if ($_POST["input_upload_type"] == 'editor') {
+				file_put_contents($fileFullName, $_POST['input_editor']);
+			} else {
+				move_uploaded_file($_FILES["input_file"]['tmp_name'], $fileFullName);
+			}
+			$input_type = isset($_POST['use_formatter']) ? "USE_FORMATTER" : "DONT_USE_FORMATTER";
+			DB::insert("insert into hacks (problem_id, submission_id, hacker, owner, input, input_type, submit_time, details, is_hidden) values ({$problem['id']}, {$submission['id']}, '{$myUser['username']}', '{$submission['submitter']}', '$fileName', '$input_type', now(), '', {$problem['is_hidden']})");
+		};
+		$hack_form->succ_href = "/hacks";
+		
+		$hack_form->runAtServer();
+	}
+
+	if ($submission['status'] == 'Judged' && hasProblemCtrlPermission($myUser, $problem)) {
+		$rejudge_form = new UOJForm('rejudge');
+		$rejudge_form->handle = function() {
+			global $submission;
+			rejudgeSubmission($submission);
+		};
+		$rejudge_form->submit_button_config['class_str'] = 'btn btn-primary';
+		$rejudge_form->submit_button_config['text'] = '重新测试';
+		$rejudge_form->submit_button_config['align'] = 'right';
+		$rejudge_form->runAtServer();
+	}
+	
+	if (isSuperUser($myUser)) {
+		$delete_form = new UOJForm('delete');
+		$delete_form->handle = function() {
+			global $submission;
+			$content = json_decode($submission['content'], true);
+			unlink(UOJContext::storagePath().$content['file_name']);
+			DB::delete("delete from submissions where id = {$submission['id']}");
+			updateBestACSubmissions($submission['submitter'], $submission['problem_id']);
+		};
+		$delete_form->submit_button_config['class_str'] = 'btn btn-danger';
+		$delete_form->submit_button_config['text'] = '删除此提交记录';
+		$delete_form->submit_button_config['align'] = 'right';
+		$delete_form->submit_button_config['smart_confirm'] = '';
+		$delete_form->succ_href = "/submissions";
+		$delete_form->runAtServer();
+	}
+	
+	$should_show_content = hasViewPermission($problem_extra_config['view_content_type'], $myUser, $problem, $submission);
+	$should_show_all_details = hasViewPermission($problem_extra_config['view_all_details_type'], $myUser, $problem, $submission);
+	$should_show_details = hasViewPermission($problem_extra_config['view_details_type'], $myUser, $problem, $submission);
+	$should_show_details_to_me = isSuperUser($myUser);
+	if (explode(', ', $submission['status'])[0] != 'Judged') {
+		$should_show_all_details = false;
+	}
+	if ($contest != null && $contest['cur_progress'] == CONTEST_IN_PROGRESS) {
+		if ($contest['extra_config']["problem_{$submission['problem_id']}"] === 'no-details') {
+			$should_show_details = false;
+		}
+	}
+	if (!isSubmissionFullVisibleToUser($submission, $contest, $problem, $myUser)) {
+		$should_show_content = $should_show_all_details = false;
+	}
+	if ($contest != null && hasContestPermission($myUser, $contest)) {
+		$should_show_details_to_me = true;
+		$should_show_content = true;
+		$should_show_all_details = true;
+	}
+	
+	if ($should_show_all_details) {
+		$styler = new SubmissionDetailsStyler();
+		if ((!$should_show_details || ($contest['extra_config']['contest_type']=='IOI' && $contest['cur_progress'] == CONTEST_IN_PROGRESS)) && !hasContestPermission($myUser, $contest)) {
+			$styler->fade_all_details = true;
+			$styler->show_small_tip = false;
+			if ($contest['extra_config']['contest_type']=='IOI' && $contest['cur_progress'] == CONTEST_IN_PROGRESS) {
+				$styler->ioi_contest_is_running = true;
+			}
+		}
+	}
+?>
+<?php 
+	$REQUIRE_LIB['hljs'] = "";
+?>
+<?php echoUOJPageHeader(UOJLocale::get('problems::submission').' #'.$submission['id']) ?>
+<?php echoSubmissionsListOnlyOne($submission, array(), $myUser) ?>
+
+<?php if ($should_show_captcha): ?>
+	<div class="card border-info mb-3">
+		<div class="card-header bg-info"><h4>需要验证码</h4></div>
+		<div class="card-body">
+			<img id="captcha-img" />
+			<?php $captcha_form->printHTML() ?>
+		</div>
+	</div>
+	<script>
+		(function () {
+			const div = document.getElementById('rmj-captcha');
+			const captcha = div.dataset.captcha;
+			const img = document.getElementById('captcha-img');
+			img.src = `data:image/jpeg;base64,${captcha}`;
+		}) ();
+	</script>
+<?php endif ?>
+
+<?php if ($should_show_content): ?>
+	<?php
+		if ($submission['problem_id'] == 260328) {
+			echoSubmissionContent($submission, json_decode('[{"name":"answer","type":"source code","file_name":"answer.code"}]', true));
+		}
+		else
+			echoSubmissionContent($submission, getProblemSubmissionRequirement($problem));
+	?>
+	<?php if ($hackable): ?>
+		<p class="text-center">
+			这程序好像有点Bug,我给组数据试试? <button id="button-display-hack" type="button" class="btn btn-danger btn-xs">Hack!</button>
+		</p>
+		<div id="div-form-hack" style="display:none" class="bot-buffer-md">
+			<?php $hack_form->printHTML() ?>
+		</div>
+		<script type="text/javascript">
+			$(document).ready(function() {
+				$('#button-display-hack').click(function() {
+					$('#div-form-hack').toggle('fast');
+				});
+			});
+		</script>
+	<?php endif ?>
+<?php endif ?>
+
+<?php if ($should_show_all_details): ?>
+	<div class="card border-info mb-3">
+		<div class="card-header bg-info">
+			<h4 class="card-title"><?= UOJLocale::get('details') ?></h4>
+		</div>
+		<div class="card-body">
+			<?php echoJudgementDetails($submission_result['details'], $styler, 'details') ?>
+			<?php if ($should_show_details_to_me): ?>
+				<?php if (isset($submission_result['final_result'])): ?>
+					<hr />
+					<?php echoSubmissionDetails($submission_result['final_result']['details'], 'final_details') ?>
+				<?php endif ?>
+				<?php if ($styler->fade_all_details): ?>
+					<hr />
+					<?php echoSubmissionDetails($submission_result['details'], 'final_details') ?>
+				<?php endif ?>
+			<?php endif ?>
+		</div>
+	</div>
+<?php endif ?>
+
+<?php if (isset($rejudge_form)): ?>
+	<?php $rejudge_form->printHTML() ?>
+<?php endif ?>
+
+<?php if (isset($delete_form)): ?>
+	<div class="top-buffer-sm">
+		<?php $delete_form->printHTML() ?>
+	</div>
+<?php endif ?>
+<?php echoUOJPageFooter() ?>

+ 35 - 0
app/controllers/submission_status_details.php

@@ -0,0 +1,35 @@
+<?php
+
+if (!Auth::check()) {
+	become403Page();
+}
+
+if (!is_array($_GET['get'])) {
+	become404Page();
+}
+
+$res = [];
+
+foreach ($_GET['get'] as $id) {
+	if (!validateUInt($id)) {
+		become404Page();
+	}
+	$submission = querySubmission($id);
+	if ($submission['submitter'] !== Auth::id() && !isSuperUser(queryUser(Auth::id()))) {
+		become403Page();
+	}
+	
+	$problem = queryProblemBrief($submission['problem_id']);
+	if (!isSubmissionVisibleToUser($submission, $problem, Auth::user())) {
+		become403Page();
+	}
+	
+	$out_status = explode(', ', $submission['status'])[0];
+	
+	$res[] = [
+		'judged' => $out_status == 'Judged',
+		'html' => getSubmissionStatusDetails($submission)
+	];
+}
+
+die(json_encode($res));

+ 83 - 0
app/controllers/submissions_list.php

@@ -0,0 +1,83 @@
+<?php
+	$conds = array();
+	
+	$q_problem_id = isset($_GET['problem_id']) && validateUInt($_GET['problem_id']) ? $_GET['problem_id'] : null;
+	$q_submitter = isset($_GET['submitter']) && validateUsername($_GET['submitter']) ? $_GET['submitter'] : null;
+	$q_min_score = isset($_GET['min_score']) && validateUInt($_GET['min_score']) ? $_GET['min_score'] : null;
+	$q_max_score = isset($_GET['max_score']) && validateUInt($_GET['max_score']) ? $_GET['max_score'] : null;
+	$q_language = isset($_GET['language']) ? $_GET['language'] : null;
+	if ($q_problem_id != null) {
+		$conds[] = "problem_id = $q_problem_id";
+	}
+	if ($q_submitter != null) {
+		$conds[] = "submitter = '$q_submitter'";
+	}
+	if ($q_min_score != null) {
+		$conds[] = "score >= $q_min_score";
+	}
+	if ($q_max_score != null) {
+		$conds[] = "score <= $q_max_score";
+	}
+	if ($q_language != null) {
+		$conds[] = sprintf("language = '%s'", DB::escape($q_language));
+	}
+	
+	$html_esc_q_language = htmlspecialchars($q_language);
+	
+	if ($conds) {
+		$cond = join($conds, ' and ');
+	} else {
+		$cond = '1';
+	}
+?>
+<?php echoUOJPageHeader(UOJLocale::get('submissions')) ?>
+<div class="d-none d-sm-block">
+	<?php if ($myUser != null): ?>
+	<div class="float-right">
+		<a href="/submissions?submitter=<?= $myUser['username'] ?>" class="btn btn-primary btn-sm"><?= UOJLocale::get('problems::my submissions') ?></a>
+	</div>
+	<?php endif ?>
+	<form id="form-search" class="form-inline" method="get">
+		<div id="form-group-problem_id" class="form-group">
+			<label for="input-problem_id" class="control-label mx-2"><?= UOJLocale::get('problems::problem id')?>:</label>
+			<input type="text" class="form-control input-sm" name="problem_id" id="input-problem_id" value="<?= $q_problem_id ?>" maxlength="4" style="width:4em" />
+		</div>
+		<div id="form-group-submitter" class="form-group">
+			<label for="input-submitter" class="control-label mx-2"><?= UOJLocale::get('username')?>:</label>
+			<input type="text" class="form-control input-sm" name="submitter" id="input-submitter" value="<?= $q_submitter ?>" maxlength="20" style="width:10em" />
+		</div>
+		<div id="form-group-score" class="form-group">
+			<label for="input-min_score" class="control-label mx-2"><?= UOJLocale::get('score range')?>:</label>
+			<input type="text" class="form-control input-sm" name="min_score" id="input-min_score" value="<?= $q_min_score ?>" maxlength="3" style="width:4em" placeholder="0" />
+			<label for="input-max_score" class="control-label">~</label>
+			<input type="text" class="form-control input-sm" name="max_score" id="input-max_score" value="<?= $q_max_score ?>" maxlength="3" style="width:4em" placeholder="100" />
+		</div>
+		<div id="form-group-language" class="form-group">
+			<label for="input-language" class="control-label mx-2"><?= UOJLocale::get('problems::language')?>:</label>
+			<input type="text" class="form-control input-sm" name="language" id="input-language" value="<?= $html_esc_q_language ?>" maxlength="10" style="width:8em" />
+		</div>
+		<button type="submit" id="submit-search" class="btn btn-secondary btn-sm ml-2"><?= UOJLocale::get('search')?></button>
+	</form>
+	<script type="text/javascript">
+		$('#form-search').submit(function(e) {
+			e.preventDefault();
+			
+			url = '/submissions';
+			qs = [];
+			$(['problem_id', 'submitter', 'min_score', 'max_score', 'language']).each(function () {
+				if ($('#input-' + this).val()) {
+					qs.push(this + '=' + encodeURIComponent($('#input-' + this).val()));
+				}
+			});
+			if (qs.length > 0) {
+				url += '?' + qs.join('&');
+			}
+			location.href = url;
+		});
+	</script>
+	<div class="top-buffer-sm"></div>
+</div>
+<?php
+	echoSubmissionsList($cond, 'order by id desc', array('judge_time_hidden' => ''), $myUser);
+?>
+<?php echoUOJPageFooter() ?>

+ 619 - 0
app/controllers/super_manage.php

@@ -0,0 +1,619 @@
+<?php
+	requirePHPLib('form');
+	requirePHPLib('judger');
+	
+	if ($myUser == null || !isSuperUser($myUser)) {
+		become403Page();
+	}
+	
+	$user_form = new UOJForm('user');
+	$user_form->addInput('username', 'text', '用户名', '',
+		function ($username) {
+			if (!validateUsername($username)) {
+				return '用户名不合法';
+			}
+			if (!queryUser($username)) {
+				return '用户不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$options = array(
+		'banneduser' => '设为封禁用户',
+		'normaluser' => '设为普通用户',
+		'superuser' => '设为超级用户'
+	);
+	$user_form->addSelect('op-type', $options, '操作类型', '');
+	$user_form->handle = function() {
+		global $user_form;
+		
+		$username = $_POST['username'];
+		switch ($_POST['op-type']) {
+			case 'banneduser':
+				DB::update("update user_info set usergroup = 'B' where username = '{$username}'");
+				break;
+			case 'normaluser':
+				DB::update("update user_info set usergroup = 'U' where username = '{$username}'");
+				break;
+			case 'superuser':
+				DB::update("update user_info set usergroup = 'S' where username = '{$username}'");
+				break;
+		}
+	};
+	$user_form->runAtServer();
+
+	$assign_certs_form = new UOJForm('assign_certs_form');
+
+	$color_options = array('red' => '红色', 'blue' => '蓝色', 'green' => '绿色', 'purple' => '紫色', 'pink' => '粉色', 'olive' => '屎黄色', 'deepblue' => '深蓝');
+	$assign_certs_form->addSelect('color-type', $color_options, '勋章颜色', '');
+	$assign_certs_form->addInput('certs_user', 'text', '用户名', '',
+		function ($username) {
+			if (!validateUsername($username))
+				return '用户名不合法';
+			if (!queryUser($username))
+				return '用户不存在';
+			return '';
+		}, null
+	);
+	
+	$assign_certs_form->addInput('certs_name', 'text', '称号', '',
+		function ($text) {
+			if (strpos($text, '#') != false || strpos($text, ',') != false)
+				return '不能含有井号或逗号';
+			if (mb_strlen($text) > 8) return '称号过长' . $text;
+			return '';
+		}, null
+	);
+
+	$assign_certs_form->handle = function() {
+		$username = $_POST['certs_user'];
+		$text = $_POST['certs_name'];
+		$color = $_POST['color-type'];
+		$user = queryUser($username);
+		$newcerts = $user['certslist'];
+		if ($user['certslist'] != '') $newcerts = $newcerts . ',' . $color . '#' . $text;
+		else $newcerts = $color . '#' . $text;
+		DB::update("update user_info set certslist = '{$newcerts}' where username = '{$username}'");
+		if ($user['certs'] == '') DB::update("update user_info set certs = '{$newcerts}' where username = '{$username}'");
+	};
+	$assign_certs_form->runAtServer();
+
+	$user_attach_form = new UOJForm('user_attach_form');
+	$user_attach_form->addInput('username', 'text', '用户名', '',
+		function ($username) {
+			if (!validateUsername($username)) {
+				return '用户名不合法';
+			}
+			if (!queryUser($username)) {
+				return '用户不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$user_attach_form->addInput('group', 'text', '组名', '',
+		function ($username) {
+			if (!validateUsername($username))
+				return '组名不合法';
+			if (!queryUser($username))
+				return '组不存在';
+			if (substr($username, 0, 6) != 'group_')
+				return '不是合法组(前缀非 group_)';
+			return '';
+		},
+		null
+	);
+
+	$user_attach_form->handle = function() {
+		$username = $_POST['username'];
+		$group = $_POST['group'];
+		attachUserToGroup (queryUser($username), queryUser($group));
+	};
+
+	$user_attach_form->runAtServer();
+
+	$problem_attach_form = new UOJForm('problem_attach_form');
+	$problem_attach_form->addInput('pid', 'text', '题号', '',
+		function ($pid) {
+			if (!validateUInt($pid)) {
+				return '题号不合法';
+			}
+			if (!queryProblemBrief($pid)) {
+				return '题目不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$problem_attach_form->addInput('problem_group', 'text', '组名', '',
+		function ($username) {
+			if (!validateUsername($username))
+				return '组名不合法';
+			if (!queryUser($username))
+				return '组不存在';
+			if (substr($username, 0, 6) != 'group_')
+				return '不是合法组(前缀非 group_)';
+			return '';
+		},
+		null
+	);
+
+	$problem_attach_form->handle = function() {
+		$pid = $_POST['pid'];
+		$group = $_POST['problem_group'];
+		attachProblemToGroup (queryProblemBrief ($pid), queryUser($group));
+	};
+
+	$problem_attach_form->runAtServer();
+
+	$blog_link_contests = new UOJForm('blog_link_contests');
+	$blog_link_contests->addInput('blog_id', 'text', '博客ID', '',
+		function ($x) {
+			if (!validateUInt($x)) {
+				return 'ID不合法';
+			}
+			if (!queryBlog($x)) {
+				return '博客不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$blog_link_contests->addInput('contest_id', 'text', '比赛ID', '',
+		function ($x) {
+			if (!validateUInt($x)) {
+				return 'ID不合法';
+			}
+			if (!queryContest($x)) {
+				return '比赛不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$blog_link_contests->addInput('title', 'text', '标题', '',
+		function ($x) {
+			return '';
+		},
+		null
+	);
+	$options = array(
+		'add' => '添加',
+		'del' => '删除'
+	);
+	$blog_link_contests->addSelect('op-type', $options, '操作类型', '');
+	$blog_link_contests->handle = function() {
+		$blog_id = $_POST['blog_id'];
+		$contest_id = $_POST['contest_id'];
+		$str = DB::selectFirst(("select * from contests where id='${contest_id}'"));
+		$all_config = json_decode($str['extra_config'], true);
+		$config = $all_config['links'];
+
+		$n = count($config);
+		
+		if ($_POST['op-type'] == 'add') {
+			$row = array();
+			$row[0] = $_POST['title'];
+			$row[1] = $blog_id;
+			$config[$n] = $row;
+		}
+		if ($_POST['op-type'] == 'del') {
+			for ($i = 0; $i < $n; $i++) {
+				if ($config[$i][1] == $blog_id) {
+					$config[$i] = $config[$n - 1];
+					unset($config[$n - 1]);
+					break;
+				}
+			}
+		}
+
+		$all_config['links'] = $config;
+		$str = json_encode($all_config);
+		$str = DB::escape($str);
+		DB::query("update contests set extra_config='${str}' where id='${contest_id}'");
+	};
+	$blog_link_contests->runAtServer();
+	
+	$blog_link_index = new UOJForm('blog_link_index');
+	$blog_link_index->addInput('blog_id2', 'text', '博客ID', '',
+		function ($x) {
+			if (!validateUInt($x)) {
+				return 'ID不合法';
+			}
+			if (!queryBlog($x)) {
+				return '博客不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$blog_link_index->addInput('blog_level', 'text', '置顶级别(删除不用填)', '0',
+		function ($x) {
+			if (!validateUInt($x)) {
+				return '数字不合法';
+			}
+			if ($x > 3) {
+				return '该级别不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$options = array(
+		'add' => '添加',
+		'del' => '删除'
+	);
+	$blog_link_index->addSelect('op-type2', $options, '操作类型', '');
+	$blog_link_index->handle = function() {
+		$blog_id = $_POST['blog_id2'];
+		$blog_level = $_POST['blog_level'];
+		if ($_POST['op-type2'] == 'add') {
+			if (DB::selectFirst("select * from important_blogs where blog_id = {$blog_id}")) {
+				DB::update("update important_blogs set level = {$blog_level} where blog_id = {$blog_id}");
+			} else {
+				DB::insert("insert into important_blogs (blog_id, level) values ({$blog_id}, {$blog_level})");
+			}
+		}
+		if ($_POST['op-type2'] == 'del') {
+			DB::delete("delete from important_blogs where blog_id = {$blog_id}");
+		}
+	};
+	$blog_link_index->runAtServer();
+	
+	$blog_deleter = new UOJForm('blog_deleter');
+	$blog_deleter->addInput('blog_del_id', 'text', '博客ID', '',
+		function ($x) {
+			if (!validateUInt($x)) {
+				return 'ID不合法';
+			}
+			if (!queryBlog($x)) {
+				return '博客不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$blog_deleter->handle = function() {
+		deleteBlog($_POST['blog_del_id']);
+	};
+	$blog_deleter->runAtServer();
+
+	$contest_submissions_deleter = new UOJForm('contest_submissions');
+	$contest_submissions_deleter->addInput('contest_id', 'text', '比赛ID', '',
+		function ($x) {
+			if (!validateUInt($x)) {
+				return 'ID不合法';
+			}
+			if (!queryContest($x)) {
+				return '博客不存在';
+			}
+			return '';
+		},
+		null
+	);
+	$contest_submissions_deleter->handle = function() {
+		$contest = queryContest($_POST['contest_id']);
+		genMoreContestInfo($contest);
+		
+		$contest_problems = DB::selectAll("select problem_id from contests_problems where contest_id = {$contest['id']}");
+		foreach ($contest_problems as $problem) {
+			$submissions = DB::selectAll("select * from submissions where problem_id = {$problem['problem_id']} and submit_time < '{$contest['start_time_str']}'");
+			foreach ($submissions as $submission) {
+				$content = json_decode($submission['content'], true);
+				unlink(UOJContext::storagePath().$content['file_name']);
+				DB::delete("delete from submissions where id = {$submission['id']}");
+				updateBestACSubmissions($submission['submitter'], $submission['problem_id']);
+			}
+		}
+	};
+	$contest_submissions_deleter->runAtServer();
+
+	$custom_test_deleter = new UOJForm('custom_test_deleter');
+	$custom_test_deleter->addInput('last', 'text', '删除末尾记录', '5',
+		function ($x, &$vdata) {
+			if (!validateUInt($x)) {
+				return '不合法';
+			}
+			$vdata['last'] = $x;
+			return '';
+		},
+		null
+	);
+	$custom_test_deleter->handle = function(&$vdata) {
+		$all = DB::selectAll("select * from custom_test_submissions order by id asc limit {$vdata['last']}");
+		foreach ($all as $submission) {
+			$content = json_decode($submission['content'], true);
+			unlink(UOJContext::storagePath().$content['file_name']);
+		}
+		DB::delete("delete from custom_test_submissions order by id asc limit {$vdata['last']}");
+	};
+	$custom_test_deleter->runAtServer();
+
+	$judger_adder = new UOJForm('judger_adder');
+	$judger_adder->addInput('judger_adder_name', 'text', '评测机名称', '',
+		function ($x, &$vdata) {
+			if (!validateUsername($x)) {
+				return '不合法';
+			}
+			if (DB::selectCount("select count(*) from judger_info where judger_name='$x'")!=0) {
+				return '不合法';
+			}
+			$vdata['name'] = $x;
+			return '';
+		},
+		null
+	);
+	$judger_adder->handle = function(&$vdata) {
+		$password=uojRandString(32);
+		DB::insert("insert into judger_info (judger_name,password) values('{$vdata['name']}','{$password}')");
+	};
+	$judger_adder->runAtServer();
+	
+	$judger_deleter = new UOJForm('judger_deleter');
+	$judger_deleter->addInput('judger_deleter_name', 'text', '评测机名称', '',
+		function ($x, &$vdata) {
+			if (!validateUsername($x)) {
+				return '不合法';
+			}
+			if (DB::selectCount("select count(*) from judger_info where judger_name='$x'")!=1) {
+				return '不合法';
+			}
+			$vdata['name'] = $x;
+			return '';
+		},
+		null
+	);
+	$judger_deleter->handle = function(&$vdata) {
+		DB::delete("delete from judger_info where judger_name='{$vdata['name']}'");
+	};
+	$judger_deleter->runAtServer();
+
+	$paste_deleter = new UOJForm('paste_deleter');
+	$paste_deleter->addInput('paste_deleter_name', 'text', 'Paste ID', '',
+		function ($x, &$vdata) {
+			if (DB::selectCount("select count(*) from pastes where `index`='$x'")==0) {
+				return '不合法';
+			}
+			$vdata['name'] = $x;
+			return '';
+		},
+		null
+	);
+	$paste_deleter->handle = function(&$vdata) {
+		DB::delete("delete from pastes where `index` = '${vdata['name']}'");
+	};
+	$paste_deleter->runAtServer();
+	
+	$judgerlist_cols = array('judger_name', 'password');
+	$judgerlist_config = array();
+	$judgerlist_header_row = <<<EOD
+	<tr>
+		<th>评测机名称</th>
+		<th>密码</th>
+	</tr>
+EOD;
+	$judgerlist_print_row = function($row) {
+		echo <<<EOD
+			<tr>
+				<td>{$row['judger_name']}</td>
+				<td>{$row['password']}</td>
+			</tr>
+EOD;
+	};
+	
+	$banlist_cols = array('username', 'usergroup');
+	$banlist_config = array();
+	$banlist_header_row = <<<EOD
+	<tr>
+		<th>用户名</th>
+	</tr>
+EOD;
+	$banlist_print_row = function($row) {
+		$hislink = getUserLink($row['username']);
+		echo <<<EOD
+			<tr>
+				<td>${hislink}</td>
+			</tr>
+EOD;
+	};
+	
+	$cur_tab = isset($_GET['tab']) ? $_GET['tab'] : 'users';
+	
+	$tabs_info = array(
+		'users' => array(
+			'name' => '用户操作',
+			'url' => "/super-manage/users"
+		),
+		'blogs' => array(
+			'name' => '博客管理',
+			'url' => "/super-manage/blogs"
+		),
+		'submissions' => array(
+			'name' => '提交记录',
+			'url' => "/super-manage/submissions"
+		),
+		'custom-test' => array(
+			'name' => '自定义测试',
+			'url' => '/super-manage/custom-test'
+		),
+		'click-zan' => array(
+			'name' => '点赞管理',
+			'url' => '/super-manage/click-zan'
+		),
+		'search' => array(
+			'name' => '搜索管理',
+			'url' => '/super-manage/search'
+		),
+		'judger' => array(
+			'name' => '评测机管理',
+			'url' => '/super-manage/judger'
+		),
+		'paste' => array(
+			'name' => 'Paste管理',
+			'url' => '/super-manage/paste'
+		),
+		'groups' => array(
+			'name' => '用户组管理',
+			'url' => '/super-manage/groups'
+		),
+		'certs' => array(
+			'name' => '勋章管理',
+			'url' => '/super-manage/certs'
+		)
+	);
+	
+	if (!isset($tabs_info[$cur_tab])) {
+		become404Page();
+	}
+?>
+<?php
+	requireLib('hljs');
+	requireLib('morris');
+?>
+<?php echoUOJPageHeader('系统管理') ?>
+<div class="row">
+	<div class="col-sm-3">
+		<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills flex-column') ?>
+	</div>
+	
+	<div class="col-sm-9">
+		<?php if ($cur_tab === 'users'): ?>
+			<?php $user_form->printHTML(); ?>
+			<h3>封禁名单</h3>
+			<?php echoLongTable($banlist_cols, 'user_info', "usergroup='B'", '', $banlist_header_row, $banlist_print_row, $banlist_config) ?>
+		<?php elseif ($cur_tab === 'blogs'): ?>
+			<div>
+				<h4>添加到比赛链接</h4>
+				<?php $blog_link_contests->printHTML(); ?>
+			</div>
+
+			<div>
+				<h4>添加到公告</h4>
+				<?php $blog_link_index->printHTML(); ?>
+			</div>
+		
+			<div>
+				<h4>删除博客</h4>
+				<?php $blog_deleter->printHTML(); ?>
+			</div>
+		<?php elseif ($cur_tab === 'submissions'): ?>
+			<div>
+				<h4>删除赛前提交记录</h4>
+				<?php $contest_submissions_deleter->printHTML(); ?>
+			</div>
+			<div>
+				<h4>测评失败的提交记录</h4>
+				<?php echoSubmissionsList("result_error = 'Judgement Failed'", 'order by id desc', array('result_hidden' => ''), $myUser); ?>
+			</div>
+		<?php elseif ($cur_tab === 'custom-test'): ?>
+		<?php $custom_test_deleter->printHTML() ?>
+		<?php
+			$submissions_pag = new Paginator(array(
+				'col_names' => array('*'),
+				'table_name' => 'custom_test_submissions',
+				'cond' => '1',
+				'tail' => 'order by id asc',
+				'page_len' => 5
+			));
+			foreach ($submissions_pag->get() as $submission) {
+				$problem = queryProblemBrief($submission['problem_id']);
+				$submission_result = json_decode($submission['result'], true);
+				echo '<dl class="dl-horizontal">';
+				echo '<dt>id</dt>';
+				echo '<dd>', "#{$submission['id']}", '</dd>';
+				echo '<dt>problem_id</dt>';
+				echo '<dd>', "#{$submission['problem_id']}", '</dd>';
+				echo '<dt>submit time</dt>';
+				echo '<dd>', $submission['submit_time'], '</dd>';
+				echo '<dt>submitter</dt>';
+				echo '<dd>', $submission['submitter'], '</dd>';
+				echo '<dt>judge_time</dt>';
+				echo '<dd>', $submission['judge_time'], '</dd>';
+				echo '</dl>';
+				echoSubmissionContent($submission, getProblemCustomTestRequirement($problem));
+				echoCustomTestSubmissionDetails($submission_result['details'], "submission-{$submission['id']}-details");
+			}
+		?>
+		<?= $submissions_pag->pagination() ?>
+		<?php elseif ($cur_tab === 'click-zan'): ?>
+		没写好QAQ
+		<?php elseif ($cur_tab === 'search'): ?>
+		<h2 class="text-center">一周搜索情况</h2>
+		<div id="search-distribution-chart-week" style="height: 250px;"></div>
+		<script type="text/javascript">
+			new Morris.Line({
+				element: 'search-distribution-chart-week',
+				data: <?= json_encode(DB::selectAll("select DATE_FORMAT(created_at, '%Y-%m-%d %h:00'), count(*) from search_requests  where created_at > now() - interval 1 week group by DATE_FORMAT(created_at, '%Y-%m-%d %h:00')")) ?>,
+				xkey: "DATE_FORMAT(created_at, '%Y-%m-%d %h:00')",
+				ykeys: ["count(*)"],
+				labels: ['number'],
+				resize: true
+			});
+		</script>
+		
+		<h2 class="text-center">一月搜索情况</h2>
+		<div id="search-distribution-chart-month" style="height: 250px;"></div>
+		<script type="text/javascript">
+			new Morris.Line({
+				element: 'search-distribution-chart-month',
+				data: <?= json_encode(DB::selectAll("select DATE_FORMAT(created_at, '%Y-%m-%d'), count(*) from search_requests  where created_at > now() - interval 1 week group by DATE_FORMAT(created_at, '%Y-%m-%d')")) ?>,
+				xkey: "DATE_FORMAT(created_at, '%Y-%m-%d')",
+				ykeys: ["count(*)"],
+				labels: ['number'],
+				resize: true
+			});
+		</script>
+		
+		<?php echoLongTable(array('*'), 'search_requests', "1", 'order by id desc',
+			'<tr><th>id</th><th>created_at</th><th>remote_addr</th><th>type</th><th>q</th><tr>',
+			function($row) {
+				echo '<tr>';
+				echo '<td>', $row['id'], '</td>';
+				echo '<td>', $row['created_at'], '</td>';
+				echo '<td>', $row['remote_addr'], '</td>';
+				echo '<td>', $row['type'], '</td>';
+				echo '<td>', HTML::escape($row['q']), '</td>';
+				echo '</tr>';
+			}, array(
+				'page_len' => 1000
+			))
+		?>
+		<?php elseif ($cur_tab === 'judger'): ?>
+			<div>
+				<h4>添加评测机</h4>
+				<?php $judger_adder->printHTML(); ?>
+			</div>
+			<div>
+				<h4>删除评测机</h4>
+				<?php $judger_deleter->printHTML(); ?>
+			</div>
+			<h3>评测机列表</h3>
+			<?php echoLongTable($judgerlist_cols, 'judger_info', "1=1", '', $judgerlist_header_row, $judgerlist_print_row, $judgerlist_config) ?>
+		<?php elseif ($cur_tab === 'paste'): ?>
+			<div>
+				<h4>Paste管理</h4>
+				<?php echoPastesList() ?>
+			</div>
+		<?php elseif ($cur_tab == 'groups'): ?>
+			<div>
+				<h4>用户组管理</h4>
+				<?php echoGroupsList() ?>
+			</div>
+			<div>
+				<h4>添加用户到组</h4>
+				<?php $user_attach_form->printHTML(); ?>
+			</div>
+			<div>
+				<h4>添加题目到组</h4>
+				<?php $problem_attach_form->printHTML(); ?>
+			</div>
+		<?php elseif ($cur_tab == 'certs'): ?>
+			<div>
+				<h4>将勋章授予某人</h4>
+				<?php $assign_certs_form->printHTML(); ?>
+			</div>
+		<?php endif ?>
+	</div>
+</div>
+<?php echoUOJPageFooter() ?>

+ 271 - 0
app/controllers/user_info.php

@@ -0,0 +1,271 @@
+<?php
+	$username = $_GET['username'];
+	
+	requireLib('flot');
+	$REQUIRE_LIB['mathjax'] = '';
+	$REQUIRE_LIB['hljs'] = '';
+
+?>
+<?php if (validateUsername($username) && ($user = queryUser($username))): ?>
+	<?php echoUOJPageHeader($user['username'] . ' - ' . UOJLocale::get('user profile')) ?>
+	<?php
+		$esc_email = HTML::escape($user['email']);
+		$esc_qq = HTML::escape($user['qq'] != 0 ? $user['qq'] : 'Unfilled');
+		$esc_sex = HTML::escape($user['sex']);
+		$col_sex="color:blue";
+		if ($esc_sex == "M") {
+			$esc_sex="♂";
+			$col_sex="color:blue";
+		} elseif ($esc_sex == "F") {
+			$esc_sex="♀";
+			$col_sex="color:red";
+		} else {
+			$esc_sex="";
+			$col_sex="color:black";
+		}
+		$esc_motto = HTML::escape($user['motto']);
+		$extgroups = $user['extgroups'];
+	?>
+	<div class="card border-info">
+		<h5 class="card-header bg-info"><?= UOJLocale::get('user profile') ?></h5>
+		<div class="card-body">
+			<div class="row mb-4">
+				<div class="col-md-4 order-md-9">
+					<img class="media-object img-thumbnail d-block mx-auto" alt="<?= $user['username'] ?> Avatar" height="250" width="250" src="<?= HTML::avatar_addr($user, 256) ?>" />
+				</div>
+				<div class="col-md-8 order-md-1">
+					<h2><span class="uoj-honor" data-rating="<?= $user['rating'] ?>" data-certs="<?= $user['certs'] ?>"><?= $user['username'] ?></span> <span><strong style="<?= $col_sex ?>"><?= $esc_sex ?></strong></span></h2>
+					<div class="list-group">
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading"><?= UOJLocale::get('rating') ?></h4>
+							<p class="list-group-item-text"><strong style="color:red"><?= $user['rating'] ?></strong></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading"><?= UOJLocale::get('email') ?></h4>
+							<p class="list-group-item-text"><?= $esc_email ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading"><?= UOJLocale::get('QQ') ?></h4>
+							<p class="list-group-item-text"><?= $esc_qq ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading"><?= UOJLocale::get('motto') ?></h4>
+							<p class="list-group-item-text"><?= $esc_motto ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">用户组信息</h4>
+							<p class="list-group-item-text"><?= $extgroups ?></p>
+						</div>
+						
+						<?php if (isSuperUser($myUser)): ?>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">register time</h4>
+							<p class="list-group-item-text"><?= $user['register_time'] ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">remote_addr</h4>
+							<p class="list-group-item-text"><?= $user['remote_addr'] ?></p>
+						</div>
+						<div class="list-group-item">
+							<h4 class="list-group-item-heading">http_x_forwarded_for</h4>
+							<p class="list-group-item-text"><?= $user['http_x_forwarded_for'] ?></p>
+						</div>
+						<?php endif ?>
+					</div>
+				</div>
+			</div>
+			<?php if (Auth::check()): ?>
+			<?php if (Auth::id() != $user['username']): ?>
+			<a type="button" class="btn btn-info btn-sm" href="/user/msg?enter=<?= $user['username'] ?>"><span class="glyphicon glyphicon-envelope"></span> <?= UOJLocale::get('send private message') ?></a>
+			<?php else: ?>
+			<a type="button" class="btn btn-info btn-sm" href="/user/modify-profile"><span class="glyphicon glyphicon-pencil"></span> <?= UOJLocale::get('modify my profile') ?></a>
+			<?php endif ?>
+			<?php endif ?>
+			
+			<a type="button" class="btn btn-success btn-sm" href="<?= HTML::blog_url($user['username'], '/') ?>"><span class="glyphicon glyphicon-arrow-right"></span> <?= UOJLocale::get('visit his blog', $username) ?></a>
+			
+			<div class="top-buffer-lg"></div>
+			<div class="list-group">
+				<?php
+					$blogid = $user['aboutme_blogid'];
+					if ($blogid) {
+						echo '<div class="list-group-item">';
+						echoBlog(queryBlog($blogid), array('show_content_only' => true, 'outside_blog_domain' => true));
+						echo '</div>';
+					}
+				?>
+				<div class="list-group-item">
+					<h4 class="list-group-item-heading"><?= UOJLocale::get('rating changes') ?></h4>
+					<div class="list-group-item-text" id="rating-plot" style="height:500px;"></div>
+				</div>
+				<div class="list-group-item">
+					<?php
+						$ac_problems = DB::selectAll("select problem_id from best_ac_submissions where submitter = '{$user['username']}'");
+					?>
+					<h4 class="list-group-item-heading"><?= UOJLocale::get('accepted problems').':'.UOJLocale::get('n problems in total', count($ac_problems))?> </h4>
+					<p class="list-group-item-text">
+					<?php
+						foreach ($ac_problems as $problem) {
+							echo '<a href="/problem/', $problem['problem_id'], '" style="display:inline-block; width:4em;">', $problem['problem_id'], '</a>';
+						}
+						if (empty($ac_problems)) {
+							echo UOJLocale::get('none');
+						}
+					?>
+					</p>
+				</div>
+			</div>
+		</div>
+	</div>
+<script type="text/javascript">
+var rating_data = [[
+<?php
+	$user_rating_min = $user_rating_max = 1500;
+	$result = DB::query("select contest_id, `rank`, user_rating from contests_registrants where username = '{$user['username']}' and has_participated = 1 order by contest_id");
+	$is_first_row = true;
+	$last_rating = 1500;
+	while ($row = DB::fetch($result)) {
+		$contest = queryContest($row['contest_id']);
+		$rating_delta = $row['user_rating'] - $last_rating;
+		if (!$is_first_row) {
+			echo "[$last_contest_time, {$row['user_rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta],";
+		} else {
+			$is_first_row = false;
+		}
+		$contest_start_time = new DateTime($contest['start_time']);
+		$last_contest_time = ($contest_start_time->getTimestamp() + $contest_start_time->getOffset()) * 1000;
+		$last_contest_name = json_encode($contest['name']);
+		$last_contest_id = $contest['id'];
+		$last_rank = $row['rank'];
+		$last_rating = $row['user_rating'];
+		
+		if ($row['user_rating'] < $user_rating_min) {
+			$user_rating_min = $row['user_rating'];
+		}
+		if ($row['user_rating'] > $user_rating_max) {
+			$user_rating_max = $row['user_rating'];
+		}
+	}
+	if ($is_first_row) {
+		$time_now_stamp = (UOJTime::$time_now->getTimestamp() + UOJTime::$time_now->getOffset()) * 1000;
+		echo "[{$time_now_stamp}, {$user['rating']}, 0]";
+	} else {
+		$rating_delta = $user['rating'] - $last_rating;
+		echo "[$last_contest_time, {$user['rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta]";
+	}
+	if ($user['rating'] < $user_rating_min) {
+		$user_rating_min = $user['rating'];
+	}
+	if ($user['rating'] > $user_rating_max) {
+		$user_rating_max = $user['rating'];
+	}
+		
+	$user_rating_min -= 400;
+	$user_rating_max += 400;
+?>
+]];
+var rating_plot = $.plot($("#rating-plot"), [{
+	color: "#3850eb",
+	label: "<?= $user['username'] ?>",
+	data: rating_data[0]
+}], {
+	series: {
+		lines: {
+			show: true
+		},
+		points: {
+			show: true
+		}
+	},
+	xaxis: {
+		mode: "time"
+	},
+	yaxis: {
+		min: <?= $user_rating_min ?>,
+		max: <?= $user_rating_max ?>
+	},
+	legend: {
+		labelFormatter: function(username) {
+			return getUserLink(username, <?= $user['rating'] ?>, false);
+		}
+	},
+	grid: {
+		clickable: true,
+		hoverable: true
+	},
+	hooks: {
+		drawBackground: [
+			function(plot, ctx) {
+				var plotOffset = plot.getPlotOffset();
+				for (var y = 0; y < plot.height(); y++) {
+					var rating = <?= $user_rating_max ?> - <?= $user_rating_max - $user_rating_min ?> * y / plot.height();
+					ctx.fillStyle = getColOfRating(rating);
+					ctx.fillRect(plotOffset.left, plotOffset.top + y, plot.width(), Math.min(5, plot.height() - y));
+				}
+			}
+		]
+	}
+});
+
+function showTooltip(x, y, contents) {
+    $('<div id="rating-tooltip">' + contents + '</div>').css({
+        position: 'absolute',
+        display: 'none',
+        top: y - 20,
+        left: x + 10,
+        border: '1px solid #fdd',
+        padding: '2px',
+        'font-size' : '11px',
+        'background-color': '#fee',
+        opacity: 0.80
+    }).appendTo("body").fadeIn(200);
+}
+
+var prev = -1;
+function onHoverRating(event, pos, item) {
+	if (prev != item.dataIndex) {
+		$("#rating-tooltip").remove();
+		var params = rating_data[item.seriesIndex][item.dataIndex];
+
+		var total = params[1];
+		var contestId = params[2];
+		if (contestId != 0) {
+			var change = params[5] > 0 ? "+" + params[5] : params[5];
+			var contestName = params[3];
+			var rank = params[4];
+			var html = "= " + total + " (" + change + "), <br/>"
+			+ "Rank: " + rank + "<br/>"
+            + '<a href="' + '/contest/' + contestId + '">' + contestName + '</a>';
+		} else {
+			var html = "= " + total + "<br/>"
+			+ "Unrated";
+		}
+		showTooltip(item.pageX, item.pageY, html);
+		prev = item.dataIndex;
+	}
+}
+$("#rating-plot").bind("plothover", function (event, pos, item) {
+    if (item) {
+    	onHoverRating(event, pos, item);
+    }
+});
+$("#rating-plot").bind("plotclick", function (event, pos, item) {
+    if (item && prev == -1) {
+    	onHoverRating(event, pos, item);
+    } else {
+		$("#rating-tooltip").fadeOut(200);
+		prev = -1;
+	}
+});
+</script>
+<?php else: ?>
+	<?php echoUOJPageHeader('不存在该用户' . ' - 用户信息') ?>
+	<div class="card border-danger">
+		<div class="card-header bg-danger">用户信息</div>
+		<div class="card-body">
+		<h4>不存在该用户</h4>
+		</div>
+	</div>
+<?php endif ?>
+
+<?php echoUOJPageFooter() ?>

+ 306 - 0
app/controllers/user_msg.php

@@ -0,0 +1,306 @@
+<?php
+	if ($myUser == null) {
+		redirectToLogin();
+	}
+
+	function handleMsgPost() {
+		global $myUser;
+		if (!isset($_POST['receiver'])) {
+			return 'fail';
+		}
+		if (!isset($_POST['message'])) {
+			return 'fail';
+		}
+		if (0 > strlen($_POST['message']) || strlen($_POST['message']) > 65535) {
+			return 'fail';
+		}
+		$receiver = $_POST['receiver'];
+		$esc_message = DB::escape($_POST['message']);
+		$sender = $myUser['username'];
+
+		if (!validateUsername($receiver) || !queryUser($receiver)) {
+			return 'fail';
+		}
+
+		DB::query("insert into user_msg (sender, receiver, message, send_time) values ('$sender', '$receiver', '$esc_message', now())");
+		return "ok";
+	}
+
+	function getConversations() {
+		global $myUser;
+		$username = $myUser['username'];
+		$result = DB::query( "select * from user_msg where sender = '$username' or receiver = '$username' order by send_time DESC" );
+		$ret = array();
+		while ($msg = DB::fetch($result)) {
+			if ($msg['sender'] !== $username) {
+				if (isset($ret[$msg['sender']])) {
+					$ret[$msg['sender']][1] |= ($msg['read_time'] == null);
+					continue;
+				}
+				$ret[$msg['sender']] = array($msg['send_time'], ($msg['read_time'] == null));
+			} else {
+				if (isset($ret[$msg['receiver']])) {
+					continue;
+				}
+				$ret[$msg['receiver']] = array($msg['send_time'], 0);
+			}
+		}
+		$res = [];
+		foreach ($ret as $name => $con) {
+			$res[] = [$con[0], $con[1], $name];
+		}
+		usort($res, function($a, $b) {
+			return -strcmp($a[0], $b[0]);
+		});
+		return json_encode($res);
+	}
+
+	function getHistory() {
+		global $myUser;
+		$username = $myUser['username'];
+		if (!isset($_GET['conversationName']) || !validateUsername($_GET['conversationName'])) {
+			return '[]';
+		}
+		if (!isset($_GET['pageNumber']) || !validateUInt($_GET['pageNumber'])) {
+			return '[]';
+		}
+
+		$conversationName = $_GET['conversationName'];
+		$pageNumber = ($_GET['pageNumber'] - 1) * 10;
+		DB::query("update user_msg set read_time = now() where sender = '$conversationName'  and  receiver = '$username' and read_time is null");
+
+		$result = DB::query("select * from user_msg where (sender = '$username' and receiver = '$conversationName') or (sender = '$conversationName' and receiver = '$username')	order by send_time DESC limit $pageNumber, 11");
+		$ret = array();
+		while ($msg = DB::fetch($result)) {
+			$ret[] = array($msg['message'], $msg['send_time'], $msg['read_time'], $msg['id'], ($msg['sender'] == $username));
+		}
+		return json_encode($ret);
+	}
+
+/*
+function deleteMsg($msgId) {
+    return 1;
+    $str = <<<EOD
+select * from user_msg
+where id = $msgId
+and read_time is null
+EOD;
+    $result = DB::query($str);
+    if (DB::fetch($result)) {
+        $str = <<<EOD
+delete from user_msg
+where id = $msgId
+EOD;
+        DB::query($str);
+        return 1;
+    }
+    return 0;
+}
+*/
+
+	if (isset($_POST['user_msg'])) {
+		die(handleMsgPost());
+	} elseif (isset($_GET['getConversations'])) {
+		die(getConversations());
+	} elseif (isset($_GET['getHistory'])) {
+		die(getHistory());
+	} 
+?>
+
+<?php echoUOJPageHeader('私信') ?>
+
+<h1 class="page-header">私信</h1>
+
+<div id="conversations">
+</div>
+
+<div id="history" style="display:none">
+	<div class="card border-primary">
+		<div class="card-header bg-primary text-white">
+			<button type="button" id="goBack" class="btn btn-info btn-sm" style="position:absolute">返回</button>
+			<div id="conversation-name" class="text-center"></div>
+		</div>
+		<div class="card-body">
+			<ul class="pagination top-buffer-no justify-content-between">
+				<li class="previous"><a class="btn btn-outline-secondary text-primary" href="#" id="pageLeft">&larr; 更早的消息</a></li>
+				<li class="text-center" id="pageShow" style="line-height:32px"></li>
+				<li class="next"><a class="btn btn-outline-secondary text-primary" href="#" id="pageRight">更新的消息 &rarr;</a></li>
+			</ul>
+			<div id="history-list" style="min-height: 200px;">
+			</div>
+			<ul class="pagination top-buffer-no justify-content-between">
+				<li class="previous"><a class="btn btn-outline-secondary text-primary" href="#history" id="pageLeft2">&larr; 更早的消息</a></li>
+				<li class="next"><a class="btn btn-outline-secondary text-primary" href="#history" id="pageRight2">更新的消息 &rarr;</a></li>
+			</ul>
+			<hr />
+			<form id="form-message">
+				<div class="form-group" id="form-group-message">
+					<textarea id="input-message" class="form-control"></textarea>
+					<span id="help-message" class="help-block"></span>
+				</div>
+				<div class="text-right">
+					<button type="submit" id="message-submit" class="btn btn-info btn-md">发送</button>
+				</div>
+			</form>
+		</div>
+	</div>
+</div>
+
+<script type="text/javascript">
+$(document).ready(function() {
+	$.ajaxSetup({async:false});
+	refreshConversations();
+	<?php if (isset($_GET['enter'])): ?>
+	enterConversation(<?= json_encode($_GET['enter']) ?>);
+	<?php endif ?>
+});
+</script>
+
+<script type="text/javascript">
+function addButton(conversationName, send_time, type) {
+	$("#conversations").append(
+		'<div class="row top-buffer-sm">' +
+			'<div class="col-sm-3">' +
+				'<button type="button" class="btn btn-' + ( type ? 'warning' : 'primary' ) + ' btn-block" ' +
+					'onclick="enterConversation(\'' + conversationName + '\')">' +
+					conversationName +
+				'</button>' +
+			'</div>' +
+			'<div class="col-sm-9" style="line-height:34px">' +
+				'最后发送时间:' + send_time +
+			'</div>' +
+		'</div>'
+	);
+}
+
+function addBubble(content, send_time, read_time, msgId, conversation, page, type) {
+	$("#history-list").append(
+			'<div style=' + (type ? "margin-left:40%;margin-right:0%;" : "margin-left:0%;margin-right:40%;") + '>' +
+				'<div class="card border-info mb-4">' +
+					'<div class="card-body" style="background:' + (type ? '#4d92d8' : '#beb7b7') + '; word-break: break-all">' +
+						'<div style="white-space:pre-wrap" ' + (type ? 'class="text-right"' : 'class="text-left"') + '>' +
+							htmlspecialchars(content) +
+						'</div>' +
+					'</div>' +
+					'<div>' +
+						'<div class="row">' +
+							'<div class="col-sm-6">' +
+								'发送时间:' + send_time +
+							'</div>' +
+							'<div class="col-sm-6 text-right">' +
+								 '查看时间:' + (read_time == null ? '<strong>未查看</strong>' : read_time) +
+							'</div>' +
+						'</div>' +
+					'</div>' +
+				'</div>' +
+			'</div>'
+	);
+}
+
+function submitMessagePost(conversationName) {
+		if ($('#input-message').val().length == 0  ||  $('#input-message').val().length >= 65536) {
+				$('#help-message').text('私信长度必须在1~65535之间。');
+				$('#form-group-message').addClass('has-error');
+				return;
+		}
+		$('#help-message').text('');
+		$('#form-group-message').removeClass('has-error');
+
+		$.post('/user/msg', {
+				user_msg : 1,
+				receiver : conversationName,
+				message : $('#input-message').val()
+    }, function(msg) {
+				$('#input-message').val("");
+		});
+}
+
+function refreshHistory(conversation, page) {
+		$("#history-list").empty();
+		var ret = false;
+		$('#conversation-name').text(conversation);
+		$('#pageShow').text("第" + page.toString() + "页");
+		$.get('/user/msg', {
+				getHistory : '',
+				conversationName : conversation,
+				pageNumber : page
+		}, function(msg) {
+				var result = JSON.parse(msg);
+				var cnt = 0, flag = 0, F = 0;
+				if (result.length == 11) flag = 1, F = 1;
+				result.reverse();
+				for (msg in result) {
+						if (flag) {flag = 0; continue;}
+						var message = result[msg];
+						addBubble(message[0], message[1], message[2], message[3], conversation, page, message[4]);
+						if ((++cnt) + 1 == result.length  &&  F) break;
+				}
+				if (result.length == 11) ret = true;
+		});
+		return ret;
+}
+
+function refreshConversations() {
+	$("#conversations").empty();
+    $.get('/user/msg', {
+			getConversations : ""
+		}, function(msg) {
+			var result = JSON.parse(msg);
+			for (i in result) {
+				var conversation = result[i];
+				if (conversation[1] == 1) {
+					addButton(conversation[2], conversation[0], conversation[1]);
+				}
+			}
+			for (i in result) {
+				var conversation = result[i];
+				if (conversation[1] == 0) {
+					addButton(conversation[2], conversation[0], conversation[1]);
+				}
+			}
+		}
+	);
+}
+
+function enterConversation(conversationName) {
+	var slideTime = 300;
+	var page = 1;
+	$("#conversations").hide(slideTime);
+    var changeAble = refreshHistory(conversationName, page);
+	$("#history").slideDown(slideTime);
+	$('#form-message').unbind("submit").submit(function() {
+		submitMessagePost(conversationName);
+		page = 1;
+		changeAble = refreshHistory(conversationName, page);
+		refreshConversations();
+		return false;
+	});
+	$('#goBack').unbind("click").click(function() {
+		refreshConversations();
+		$("#history").slideUp(slideTime);
+		$("#conversations").show(slideTime);
+		return;
+	});
+	$('#pageLeft').unbind("click").click(function() {
+		if (changeAble) page++;
+		changeAble = refreshHistory(conversationName, page);
+		return false;
+	});
+	$('#pageLeft2').unbind("click").click(function() {
+		if (changeAble) page++;
+		changeAble = refreshHistory(conversationName, page);
+	});
+	$('#pageRight').unbind("click").click(function() {
+		if (page > 1) page--;
+		changeAble = refreshHistory(conversationName, page);
+		return false;
+	});
+	$('#pageRight2').unbind("click").click(function() {
+		if (page > 1) page--;
+		changeAble = refreshHistory(conversationName, page);
+	});
+}
+
+</script>
+
+<?php echoUOJPageFooter() ?>

+ 26 - 0
app/controllers/user_system_msg.php

@@ -0,0 +1,26 @@
+<?php
+	if ($myUser == null) {
+		redirectToLogin();
+	}
+	
+	$header_row = <<<EOD
+<tr>
+	<th>消息</th>
+	<th style="width:15em">时间</th>
+</tr>
+EOD;
+	function echoSysMsg($msg) {
+		echo $msg['read_time'] == null ? '<tr class="table-warning">' : '<tr>';
+		echo '<td>';
+		echo '<h4>'.$msg['title'].'</h4>';
+		echo $msg['content'];
+		echo '</td>';
+		echo '<td>'.$msg['send_time'].'</td>';
+		echo '</tr>';
+	}
+?>
+<?php echoUOJPageHeader('系统消息') ?>
+<h2>系统消息</h2>
+<?php echoLongTable(array('*'), 'user_system_msg', "receiver='" . Auth::id() . "'", 'order by id desc', $header_row, 'echoSysMsg', array('table_classes' => array('table'))) ?>
+<?php DB::update("update user_system_msg set read_time = now() where receiver = '" . Auth::id() . "'") ?>
+<?php echoUOJPageFooter() ?>

BIN
app/libs/.uoj-contest-lib.php.swp


BIN
app/libs/.uoj-html-lib.php.swo


BIN
app/libs/.uoj-query-lib.php.swo


+ 243 - 0
app/libs/uoj-contest-lib.php

@@ -0,0 +1,243 @@
+<?php
+define("CONTEST_NOT_STARTED", 0);
+define("CONTEST_IN_PROGRESS", 1);
+define("CONTEST_PENDING_FINAL_TEST", 2);
+define("CONTEST_TESTING", 10);
+define("CONTEST_FINISHED", 20);	
+
+function calcRating($standings, $K = 400) {
+	$DELTA = 500;
+
+	$n = count($standings);
+	
+	$rating = array();
+	for ($i = 0; $i < $n; ++$i) {
+		$rating[$i] = $standings[$i][2][1];
+	}
+	
+	$rank = array();
+	$foot = array();
+	for ($i = 0; $i < $n; ) {
+		$j = $i;
+		while ($j + 1 < $n && $standings[$j + 1][3] == $standings[$j][3]) {
+			++$j;
+		}
+		$our_rk = 0.5 * (($i + 1) + ($j + 1));
+		while ($i <= $j) {
+			$rank[$i] = $our_rk;
+			$foot[$i] = $n - $rank[$i];
+			$i++;
+		}
+	}
+	
+	$weight = array();
+	for ($i = 0; $i < $n; ++$i) {
+		$weight[$i] = pow(7, $rating[$i] / $DELTA);
+	}
+	$exp = array_fill(0, $n, 0);
+	for ($i = 0; $i < $n; ++$i) {
+		for ($j = 0; $j < $n; ++$j) {
+			if ($j != $i) {
+				$exp[$i] += $weight[$i] / ($weight[$i] + $weight[$j]);
+			}
+		}
+	}
+	
+	$new_rating = array();
+	for ($i = 0; $i < $n; $i++) {
+		$new_rating[$i] = $rating[$i];
+		$new_rating[$i] += ceil($K * ($foot[$i] - $exp[$i]) / ($n - 1));
+	}
+	
+	for ($i = $n - 1; $i >= 0; $i--) {
+		if ($i + 1 < $n && $standings[$i][3] != $standings[$i + 1][3]) {
+			break;
+		}
+		if ($new_rating[$i] > $rating[$i]) {
+			$new_rating[$i] = $rating[$i];
+		}
+	}
+	
+	for ($i = 0; $i < $n; $i++) {
+		if ($new_rating[$i] < 0) {
+			$new_rating[$i] = 0;
+		}
+	}
+	
+	return $new_rating;
+}
+
+function calcRatingSelfTest() {
+	$tests = [
+		[[1500, 1], [1500, 1]],
+		[[1500, 1], [1600, 1]],
+		[[1500, 1], [1600, 2], [1600, 2]],
+		[[1500, 1], [200, 2], [100, 2]],
+		[[1500, 1], [100, 2], [200, 2]],
+		[[1500, 1], [100, 2], [200, 3]],
+		[[1500, 1], [200, 2], [100, 3]],
+		[[1500, 1], [3000, 2], [1500, 3]],
+		[[1500, 1], [3000, 2], [1500, 3], [1500, 3]],
+		[[1500, 1], [1500, 2], [1500, 3], [3000, 4]],
+		[[1500, 1], [1500, 2], [10, 3], [1, 4]]
+	];
+	foreach ($tests as $test_num => $test) {
+		print "test #{$test_num}\n";
+		
+		$standings = array();
+		$n = count($test);
+		for ($i = 0; $i < $n; $i++) {
+			$standings[] = [0, 0, [(string)$i, $test[$i][0]], $test[$i][1]];
+		}
+		$new_rating = calcRating($standings);
+		
+		for ($i = 0; $i < $n; $i++) {
+			printf("%3d: %4d -> %4d delta: %+4d\n", $test[$i][1], $test[$i][0], $new_rating[$i], $new_rating[$i] - $test[$i][0]);
+		}
+		print "\n";
+	}
+}
+
+function genMoreContestInfo(&$contest) {
+	$contest['start_time_str'] = $contest['start_time'];
+	$contest['start_time'] = new DateTime($contest['start_time']);
+	$contest['end_time'] = clone $contest['start_time'];
+	$contest['end_time']->add(new DateInterval("PT${contest['last_min']}M"));
+	
+	if ($contest['status'] == 'unfinished') {
+		if (UOJTime::$time_now < $contest['start_time']) {
+			$contest['cur_progress'] = CONTEST_NOT_STARTED;
+		} elseif (UOJTime::$time_now < $contest['end_time']) {
+			$contest['cur_progress'] = CONTEST_IN_PROGRESS;
+		} else {
+			$contest['cur_progress'] = CONTEST_PENDING_FINAL_TEST;
+		}
+	} elseif ($contest['status'] == 'testing') {
+		$contest['cur_progress'] = CONTEST_TESTING;
+	} elseif ($contest['status'] == 'finished') {
+		$contest['cur_progress'] = CONTEST_FINISHED;
+	}
+	$contest['extra_config'] = json_decode($contest['extra_config'], true);
+	
+	if (!isset($contest['extra_config']['standings_version'])) {
+		$contest['extra_config']['standings_version'] = 2;
+	}
+}
+
+function updateContestPlayerNum($contest) {
+	DB::update("update contests set player_num = (select count(*) from contests_registrants where contest_id = {$contest['id']}) where id = {$contest['id']}");
+}
+
+// problems: pos => id
+// data    : id, submit_time, submitter, problem_pos, score
+// people  : username, user_rating
+function queryContestData($contest, $config = array()) {
+	mergeConfig($config, [
+		'pre_final' => false
+	]);
+	
+	$problems = [];
+	$prob_pos = [];
+	$n_problems = 0;
+	$result = DB::query("select problem_id from contests_problems where contest_id = {$contest['id']} order by problem_id");
+	while ($row = DB::fetch($result, MYSQLI_NUM)) {
+		$prob_pos[$problems[] = (int)$row[0]] = $n_problems++;
+	}
+
+	$data = [];
+	if ($config['pre_final']) {
+		$result = DB::query("select id, submit_time, submitter, problem_id, result from submissions"
+				." where contest_id = {$contest['id']} and score is not null order by id");
+		while ($row = DB::fetch($result, MYSQLI_NUM)) {
+			$r = json_decode($row[4], true);
+			if (!isset($r['final_result'])) {
+				continue;
+			}
+			$row[0] = (int)$row[0];
+			$row[3] = $prob_pos[$row[3]];
+			$row[4] = (int)($r['final_result']['score']);
+			$data[] = $row;
+		}
+	} else {
+		if ($contest['cur_progress'] < CONTEST_FINISHED) {
+			$result = DB::query("select id, submit_time, submitter, problem_id, score from submissions"
+				." where contest_id = {$contest['id']} and score is not null order by id");
+		} else {
+			$result = DB::query("select submission_id, date_add('{$contest['start_time_str']}', interval penalty second),"
+				." submitter, problem_id, score from contests_submissions where contest_id = {$contest['id']}");
+		}
+		while ($row = DB::fetch($result, MYSQLI_NUM)) {
+			$row[0] = (int)$row[0];
+			$row[3] = $prob_pos[$row[3]];
+			$row[4] = (int)$row[4];
+			$data[] = $row;
+		}
+	}
+
+	$people = [];
+	$result = DB::query("select username, user_rating from contests_registrants where contest_id = {$contest['id']} and has_participated = 1");
+	while ($row = DB::fetch($result, MYSQLI_NUM)) {
+		$row[1] = (int)$row[1];
+		$people[] = $row;
+	}
+
+	return ['problems' => $problems, 'data' => $data, 'people' => $people];
+}
+
+function calcStandings($contest, $contest_data, &$score, &$standings, $update_contests_submissions = false) {
+	// score: username, problem_pos => score, penalty, id
+	$score = array();
+	$n_people = count($contest_data['people']);
+	$n_problems = count($contest_data['problems']);
+	foreach ($contest_data['people'] as $person) {
+		$score[$person[0]] = array();
+	}
+	foreach ($contest_data['data'] as $submission) {
+		$penalty = (new DateTime($submission[1]))->getTimestamp() - $contest['start_time']->getTimestamp();
+		if ($contest['extra_config']['standings_version'] >= 2) {
+			if ($submission[4] == 0) {
+				$penalty = 0;
+			}
+		}
+		$score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]);
+	}
+
+	// standings: rank => score, penalty, [username, user_rating], virtual_rank
+	$standings = array();
+	foreach ($contest_data['people'] as $person) {
+		$cur = array(0, 0, $person);
+		for ($i = 0; $i < $n_problems; $i++) {
+			if (isset($score[$person[0]][$i])) {
+				$cur_row = $score[$person[0]][$i];
+				$cur[0] += $cur_row[0];
+				$cur[1] += $cur_row[1];
+				if ($update_contests_submissions) {
+					DB::insert("insert into contests_submissions (contest_id, submitter, problem_id, submission_id, score, penalty) values ({$contest['id']}, '{$person[0]}', {$contest_data['problems'][$i]}, {$cur_row[2]}, {$cur_row[0]}, {$cur_row[1]})");
+				}
+			}
+		}
+		$standings[] = $cur;
+	}
+
+	usort($standings, function($lhs, $rhs) {
+		if ($lhs[0] != $rhs[0]) {
+			return $rhs[0] - $lhs[0];
+		} elseif ($lhs[1] != $rhs[1]) {
+			return $lhs[1] - $rhs[1];
+		} else {
+			return strcmp($lhs[2][0], $rhs[2][0]);
+		}
+	});
+
+	$is_same_rank = function($lhs, $rhs) {
+		return $lhs[0] == $rhs[0] && $lhs[1] == $rhs[1];
+	};
+
+	for ($i = 0; $i < $n_people; $i++) {
+		if ($i == 0 || !$is_same_rank($standings[$i - 1], $standings[$i])) {
+			$standings[$i][] = $i + 1;
+		} else {
+			$standings[$i][] = $standings[$i - 1][3];
+		}
+	}
+}

+ 368 - 0
app/libs/uoj-data-lib.php

@@ -0,0 +1,368 @@
+<?php
+	// Actually, these things should be done by main_judger so that the code would be much simpler.
+	// However, this lib exists due to some history issues.
+	
+	function dataNewProblem($id) {
+		mkdir("/var/uoj_data/upload/$id");
+		mkdir("/var/uoj_data/$id");
+		
+		exec("cd /var/uoj_data; rm $id.zip; zip $id.zip $id -r -q");
+	}
+
+	class UOJProblemConfException extends Exception {
+		public function __construct($message) {
+			parent::__construct("<strong>problem.conf</strong> : $message");
+		}
+	}
+	class UOJFileNotFoundException extends Exception {
+		public function __construct($file_name) {
+			parent::__construct("file <strong>" . htmlspecialchars($file_name) . '</strong> not found');
+		}
+	}
+
+	function dataCopyProblemData($src, $dst) {
+		exec("rm -r /var/uoj_data/upload/$dst");
+		exec("cp -r /var/uoj_data/$src /var/uoj_data/$dst");
+		exec("cd /var/uoj_data; zip $dst.zip $dst -r -q");
+	}
+	
+	function dataClearProblemData($problem) {
+		$id = $problem['id'];
+		if (!validateUInt($id)) {
+			error_log("dataClearProblemData: hacker detected");
+			return "invalid problem id";
+		}
+		
+		exec("rm /var/uoj_data/upload/$id -r");
+		exec("rm /var/uoj_data/$id -r");
+		dataNewProblem($id);
+	}
+	
+	class SyncProblemDataHandler {
+		private $problem, $user;
+		private $upload_dir, $data_dir, $prepare_dir;
+		private $requirement, $problem_extra_config;
+		private $problem_conf, $final_problem_conf;
+		private $allow_files;
+		
+		public function __construct($problem, $user) {
+			$this->problem = $problem;
+			$this->user = $user;
+		}
+		
+		private function check_conf_on($name) {
+			return isset($this->problem_conf[$name]) && $this->problem_conf[$name] == 'on';
+		}
+		
+		private function copy_to_prepare($file_name) {
+			global $uojMainJudgerWorkPath;
+			if (!isset($this->allow_files[$file_name])) {
+				throw new UOJFileNotFoundException($file_name);
+			}
+			$src = escapeshellarg("{$this->upload_dir}/$file_name");
+			$dest = escapeshellarg("{$this->prepare_dir}/$file_name");
+			if (isset($this->problem_extra_config['dont_use_formatter']) || !is_file("{$this->upload_dir}/$file_name")) {
+				exec("cp $src $dest -r", $output, $ret);
+			} else {
+				exec("$uojMainJudgerWorkPath/run/formatter <$src >$dest", $output, $ret);
+			}
+			if ($ret) {
+				throw new UOJFileNotFoundException($file_name);
+			}
+		}
+		private function copy_file_to_prepare($file_name) {
+			global $uojMainJudgerWorkPath;
+			if (!isset($this->allow_files[$file_name]) || !is_file("{$this->upload_dir}/$file_name")) {
+				throw new UOJFileNotFoundException($file_name);
+			}
+			$this->copy_to_prepare($file_name);
+		}
+		private function compile_at_prepare($name, $config = array()) {
+			global $uojMainJudgerWorkPath;
+			$include_path = "$uojMainJudgerWorkPath/include";
+			
+			if (!isset($config['src'])) {
+				$config['src'] = "$name.cpp";
+			}
+			
+			if (isset($config['path'])) {
+				exec("mv {$this->prepare_dir}/$name.cpp {$this->prepare_dir}/{$config['path']}/$name.cpp");
+				$work_path = "{$this->prepare_dir}/{$config['path']}";
+			} else {
+				$work_path = $this->prepare_dir;
+			}
+
+			$cmd_prefix = "$uojMainJudgerWorkPath/run/run_program >{$this->prepare_dir}/run_compiler_result.txt --in=/dev/null --out=stderr --err={$this->prepare_dir}/compiler_result.txt --tl=10 --ml=512 --ol=64 --type=compiler --work-path={$work_path}";
+			if (isset($config['need_include_header']) && $config['need_include_header']) {
+				exec("$cmd_prefix --add-readable-raw=$include_path/ /usr/bin/g++ -o $name {$config['src']} -I$include_path -lm -O2 -DONLINE_JUDGE");
+			} else {
+				exec("$cmd_prefix /usr/bin/g++ -o $name {$config['src']} -lm -O2 -DONLINE_JUDGE");
+			}
+			
+			$fp = fopen("{$this->prepare_dir}/run_compiler_result.txt", "r");
+			if (fscanf($fp, '%d %d %d %d', $rs, $used_time, $used_memory, $exit_code) != 4) {
+				$rs = 7;
+			}
+			fclose($fp);
+			
+			unlink("{$this->prepare_dir}/run_compiler_result.txt");
+			
+			if ($rs != 0 || $exit_code != 0) {
+				if ($rs == 0) {
+					throw new Exception("<strong>$name</strong> : compile error<pre>\n" . uojFilePreview("{$this->prepare_dir}/compiler_result.txt", 100) . "\n</pre>");
+				} elseif ($rs == 7) {
+					throw new Exception("<strong>$name</strong> : compile error. No comment");
+				} else {
+					throw new Exception("<strong>$name</strong> : compile error. Compiler " . judgerCodeStr($rs));
+				}
+			}
+			
+			unlink("{$this->prepare_dir}/compiler_result.txt");
+			
+			if (isset($config['path'])) {
+				exec("mv {$this->prepare_dir}/{$config['path']}/$name.cpp {$this->prepare_dir}/$name.cpp");
+				exec("mv {$this->prepare_dir}/{$config['path']}/$name {$this->prepare_dir}/$name");
+			}
+		}
+		private function makefile_at_prepare() {
+			global $uojMainJudgerWorkPath;
+			
+			$include_path = "$uojMainJudgerWorkPath/include";
+			$cmd_prefix = "$uojMainJudgerWorkPath/run/run_program >{$this->prepare_dir}/run_makefile_result.txt --in=/dev/null --out=stderr --err={$this->prepare_dir}/makefile_result.txt --tl=10 --ml=512 --ol=64 --type=compiler --work-path={$this->prepare_dir}";
+			exec("$cmd_prefix --add-readable-raw=$include_path/ /usr/bin/make INCLUDE_PATH=$include_path");
+			
+			$fp = fopen("{$this->prepare_dir}/run_makefile_result.txt", "r");
+			if (fscanf($fp, '%d %d %d %d', $rs, $used_time, $used_memory, $exit_code) != 4) {
+				$rs = 7;
+			}
+			fclose($fp);
+			
+			unlink("{$this->prepare_dir}/run_makefile_result.txt");
+			
+			if ($rs != 0 || $exit_code != 0) {
+				if ($rs == 0) {
+					throw new Exception("<strong>Makefile</strong> : compile error<pre>\n" . uojFilePreview("{$this->prepare_dir}/makefile_result.txt", 100) . "\n</pre>");
+				} elseif ($rs == 7) {
+					throw new Exception("<strong>Makefile</strong> : compile error. No comment");
+				} else {
+					throw new Exception("<strong>Makefile</strong> : compile error. Compiler " . judgerCodeStr($rs));
+				}
+			}
+			
+			unlink("{$this->prepare_dir}/makefile_result.txt");
+		}
+		
+		public function handle() {
+			$id = $this->problem['id'];
+			if (!validateUInt($id)) {
+				error_log("dataSyncProblemData: hacker detected");
+				return "invalid problem id";
+			}
+
+			$this->upload_dir = "/var/uoj_data/upload/$id";
+			$this->data_dir = "/var/uoj_data/$id";
+			$this->prepare_dir = "/var/uoj_data/prepare_$id";
+
+			if (file_exists($this->prepare_dir)) {
+				return "please wait until the last sync finish";
+			}
+
+			try {
+				$this->requirement = array();
+				$this->problem_extra_config = json_decode($this->problem['extra_config'], true);
+
+				mkdir($this->prepare_dir, 0755);
+				if (!is_file("{$this->upload_dir}/problem.conf")) {
+					throw new UOJFileNotFoundException("problem.conf");
+				}
+
+				$this->problem_conf = getUOJConf("{$this->upload_dir}/problem.conf");
+				$this->final_problem_conf = $this->problem_conf;
+				if ($this->problem_conf === -1) {
+					throw new UOJFileNotFoundException("problem.conf");
+				} elseif ($this->problem_conf === -2) {
+					throw new UOJProblemConfException("syntax error");
+				}
+
+				$this->allow_files = array_flip(array_filter(scandir($this->upload_dir), function($x) {
+					return $x !== '.' && $x !== '..';
+				}));
+
+				$zip_file = new ZipArchive();
+				if ($zip_file->open("{$this->prepare_dir}/download.zip", ZipArchive::CREATE) !== true) {
+					throw new Exception("<strong>download.zip</strong> : failed to create the zip file");
+				}
+				
+				if (isset($this->allow_files['require']) && is_dir("{$this->upload_dir}/require")) {
+					$this->copy_to_prepare('require');
+				}
+
+				if ($this->check_conf_on('use_builtin_judger')) {
+					$n_tests = getUOJConfVal($this->problem_conf, 'n_tests', 10);
+					if (!validateUInt($n_tests) || $n_tests <= 0) {
+						throw new UOJProblemConfException("n_tests must be a positive integer");
+					}
+					for ($num = 1; $num <= $n_tests; $num++) {
+						$input_file_name = getUOJProblemInputFileName($this->problem_conf, $num);
+						$output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num);
+
+						$this->copy_file_to_prepare($input_file_name);
+						$this->copy_file_to_prepare($output_file_name);
+					}
+
+					if (!$this->check_conf_on('interaction_mode')) {
+						if (isset($this->problem_conf['use_builtin_checker'])) {
+							if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $this->problem_conf['use_builtin_checker'])) {
+								throw new Exception("<strong>" . htmlspecialchars($this->problem_conf['use_builtin_checker']) . "</strong> is not a valid checker");
+							}
+						} else {
+							$this->copy_file_to_prepare('chk.cpp');
+							$this->compile_at_prepare('chk', array('need_include_header' => true));
+						}
+					}
+					
+					if ($this->check_conf_on('submit_answer')) {
+						if ($this->problem['hackable']) {
+							throw new UOJProblemConfException("the problem can't be hackable if submit_answer is on");
+						}
+
+						for ($num = 1; $num <= $n_tests; $num++) {
+							$input_file_name = getUOJProblemInputFileName($this->problem_conf, $num);
+							$output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num);
+							
+							if (!isset($this->problem_extra_config['dont_download_input'])) {
+								$zip_file->addFile("{$this->prepare_dir}/$input_file_name", "$input_file_name");
+							}
+
+							$this->requirement[] = array('name' => "output$num", 'type' => 'text', 'file_name' => $output_file_name);
+						}
+					} else {
+						$n_ex_tests = getUOJConfVal($this->problem_conf, 'n_ex_tests', 0);
+						if (!validateUInt($n_ex_tests) || $n_ex_tests < 0) {
+							throw new UOJProblemConfException("n_ex_tests must be a non-negative integer");
+						}
+
+						for ($num = 1; $num <= $n_ex_tests; $num++) {
+							$input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num);
+							$output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num);
+
+							$this->copy_file_to_prepare($input_file_name);
+							$this->copy_file_to_prepare($output_file_name);
+						}
+
+						if ($this->problem['hackable']) {
+							$this->copy_file_to_prepare('std.cpp');
+							if (isset($this->problem_conf['with_implementer']) && $this->problem_conf['with_implementer'] == 'on') {
+								$this->compile_at_prepare('std',
+									array(
+										'src' => 'implementer.cpp std.cpp',
+										'path' => 'require'
+									)
+								);
+							} else {
+								$this->compile_at_prepare('std');
+							}
+							$this->copy_file_to_prepare('val.cpp');
+							$this->compile_at_prepare('val', array('need_include_header' => true));
+						}
+						
+						if ($this->check_conf_on('interaction_mode')) {
+							$this->copy_file_to_prepare('interactor.cpp');
+							$this->compile_at_prepare('interactor', array('need_include_header' => true));
+						}
+
+						$n_sample_tests = getUOJConfVal($this->problem_conf, 'n_sample_tests', $n_tests);
+						if (!validateUInt($n_sample_tests) || $n_sample_tests < 0) {
+							throw new UOJProblemConfException("n_sample_tests must be a non-negative integer");
+						}
+						if ($n_sample_tests > $n_ex_tests) {
+							throw new UOJProblemConfException("n_sample_tests can't be greater than n_ex_tests");
+						}
+
+						if (!isset($this->problem_extra_config['dont_download_sample'])) {
+							for ($num = 1; $num <= $n_sample_tests; $num++) {
+								$input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num);
+								$output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num);
+								$zip_file->addFile("{$this->prepare_dir}/{$input_file_name}", "$input_file_name");
+								if (!isset($this->problem_extra_config['dont_download_sample_output'])) {
+									$zip_file->addFile("{$this->prepare_dir}/{$output_file_name}", "$output_file_name");
+								}
+							}
+						}
+
+						$this->requirement[] = array('name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code');
+					}
+				} else {
+					if (!isSuperUser($this->user)) {
+						throw new UOJProblemConfException("use_builtin_judger must be on.");
+					} else {
+						foreach ($this->allow_files as $file_name => $file_num) {
+							$this->copy_to_prepare($file_name);
+						}
+						$this->makefile_at_prepare();
+						
+						$this->requirement[] = array('name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code');
+					}
+				}
+				putUOJConf("{$this->prepare_dir}/problem.conf", $this->final_problem_conf);
+
+				if (isset($this->allow_files['download']) && is_dir("{$this->upload_dir}/download")) {
+					foreach (scandir("{$this->upload_dir}/download") as $file_name) {
+						if (is_file("{$this->upload_dir}/download/{$file_name}")) {
+							$zip_file->addFile("{$this->upload_dir}/download/{$file_name}", $file_name);
+						}
+					}
+				}
+				
+				$zip_file->close();
+
+				$orig_requirement = json_decode($this->problem['submission_requirement'], true);
+				if (!$orig_requirement) {
+					$esc_requirement = DB::escape(json_encode($this->requirement));
+					DB::update("update problems set submission_requirement = '$esc_requirement' where id = $id");
+				}
+			} catch (Exception $e) {
+				exec("rm {$this->prepare_dir} -r");
+				return $e->getMessage();
+			}
+
+			exec("rm {$this->data_dir} -r");
+			rename($this->prepare_dir, $this->data_dir);
+		
+			exec("cd /var/uoj_data; rm $id.zip; zip $id.zip $id -r -q");
+
+			return '';
+		}
+	}
+	
+	function dataSyncProblemData($problem, $user = null) {
+		return (new SyncProblemDataHandler($problem, $user))->handle();
+	}
+	function dataAddExtraTest($problem, $input_file_name, $output_file_name) {
+		$id = $problem['id'];
+
+		$cur_dir = "/var/uoj_data/upload/$id";
+
+		# BY ryp!!!!, 20250827
+		exec ("ln -sf /var/uoj_data/$id /var/uoj_data/upload/$id");
+		
+		$problem_conf = getUOJConf("{$cur_dir}/problem.conf");
+		if ($problem_conf == -1 || $problem_conf == -2) {
+			return $problem_conf;
+		}
+		$problem_conf['n_ex_tests'] = getUOJConfVal($problem_conf, 'n_ex_tests', 0) + 1;
+		
+		$new_input_name = getUOJProblemExtraInputFileName($problem_conf, $problem_conf['n_ex_tests']);
+		$new_output_name = getUOJProblemExtraOutputFileName($problem_conf, $problem_conf['n_ex_tests']);
+		
+		putUOJConf("$cur_dir/problem.conf", $problem_conf);
+		move_uploaded_file($input_file_name, "$cur_dir/$new_input_name");
+		move_uploaded_file($output_file_name, "$cur_dir/$new_output_name");
+		
+		if (dataSyncProblemData($problem) === '') {
+			rejudgeProblemAC($problem);
+		} else {
+			error_log('hack successfully but sync failed.');
+		}
+	}
+?>

+ 809 - 0
app/libs/uoj-form-lib.php

@@ -0,0 +1,809 @@
+<?php
+	class UOJForm {
+		public $form_name;
+		public $succ_href;
+		public $no_submit = false;
+		public $ctrl_enter_submit = false;
+		public $extra_validator = null;
+		public $is_big = false;
+		public $has_file = false;
+		public $ajax_submit_js = null;
+		public $run_at_server_handler = array();
+		private $data = array();
+		private $vdata = array();
+		private $main_html = '';
+		public $max_post_size = 15728640; // 15M
+		
+		public $handle;
+		
+		public $submit_button_config = array();
+		
+		public function __construct($form_name) {
+			$this->form_name = $form_name;
+			$this->succ_href = $_SERVER['REQUEST_URI'];
+			$this->handle = function(&$vdata) {
+			};
+			
+			$this->run_at_server_handler["check-{$this->form_name}"] = function() {
+				die(json_encode($this->validateAtServer()));
+			};
+			$this->run_at_server_handler["submit-{$this->form_name}"] = function() {
+				if ($this->no_submit) {
+					become404Page();
+				}
+				foreach ($this->data as $field) {
+					if (!isset($field['no_val']) && !isset($_POST[$field['name']])) {
+						becomeMsgPage('The form is incomplete.');
+					}
+				}
+				
+				if (UOJContext::requestMethod() == 'POST') {
+					$len = UOJContext::contentLength();
+					if ($len === null) {
+						become403Page();
+					} elseif ($len > $this->max_post_size) {
+						becomeMsgPage('The form is too large.');
+					}
+				}
+				
+				crsf_defend();
+				$errors = $this->validateAtServer();
+				if ($errors) {
+					$err_str = '';
+					foreach ($errors as $name => $err) {
+						$esc_err = htmlspecialchars($err);
+						$err_str .= "$name: $esc_err<br />";
+					}
+					becomeMsgPage($err_str);
+				}
+				$fun = $this->handle;
+				$fun($this->vdata);
+				
+				if ($this->succ_href !== 'none') {
+					header("Location: {$this->succ_href}");
+				}
+				die();
+			};
+		}
+
+		public function setAjaxSubmit($js) {
+			$GLOBALS['REQUIRE_LIB']['jquery.form'] = '';
+			$this->ajax_submit_js = $js;
+		}
+		
+		public function add($name, $html, $validator_php, $validator_js) {
+			$this->main_html .= $html;
+			$this->data[] = array(
+				'name' => $name,
+				'validator_php' => $validator_php,
+				'validator_js' => $validator_js);
+		}
+		public function appendHTML($html) {
+			$this->main_html .= $html;
+		}
+		
+		public function addNoVal($name, $html) {
+			$this->main_html .= $html;
+			$this->data[] = array(
+				'name' => $name,
+				'validator_js' => 'always_ok',
+				'no_val' => '');
+		}
+		
+		public function addHidden($name, $default_value, $validator_php, $validator_js) {
+			$default_value = htmlspecialchars($default_value);
+			$html = <<<EOD
+<input type="hidden" name="$name" id="input-$name" value="$default_value" />
+EOD;
+			$this->add($name, $html, $validator_php, $validator_js);
+		}
+		
+		public function addInput($name, $type, $label_text, $default_value, $validator_php, $validator_js) {
+			$default_value = htmlspecialchars($default_value);
+			$html = <<<EOD
+<div id="div-$name" class="form-group">
+	<label for="input-$name" class="col-sm-2 control-label">$label_text</label>
+	<div class="col-sm-3">
+		<input type="$type" class="form-control" name="$name" id="input-$name" value="$default_value" />
+		<span class="help-block" id="help-$name"></span>
+	</div>
+</div>
+EOD;
+			$this->add($name, $html, $validator_php, $validator_js);
+		}
+		public function addSelect($name, $options, $label_text, $default_value) {
+			$default_value = htmlspecialchars($default_value);
+			$html = <<<EOD
+<div id="div-$name" class="form-group">
+	<label for="input-$name" class="col-sm-2 control-label">$label_text</label>
+	<div class="col-sm-3">
+		<select class="form-control" id="input-content" name="$name">
+
+EOD;
+			foreach ($options as $opt_name => $opt_label) {
+				if ($opt_name != $default_value) {
+					$html .= <<<EOD
+			<option value="$opt_name">$opt_label</option>
+
+EOD;
+				} else {
+					$html .= <<<EOD
+			<option value="$opt_name" selected="selected">$opt_label</option>
+
+EOD;
+				}
+			}
+			$html .= <<<EOD
+		</select>
+	</div>
+</div>
+EOD;
+			$this->add($name, $html,
+				function($opt) use ($options) {
+					return isset($options[$opt]) ? '' : "无效选项";
+				},
+				null
+			);
+		}
+		
+		public function addVInput($name, $type, $label_text, $default_value, $validator_php, $validator_js) {
+			$html = HTML::div_vinput($name, $type, $label_text, $default_value);
+			$this->add($name, $html, $validator_php, $validator_js);
+		}
+		public function addVSelect($name, $options, $label_text, $default_value) {
+			$default_value = htmlspecialchars($default_value);
+			$html = <<<EOD
+<div id="div-$name">
+	<label for="input-$name" class="control-label">$label_text</label>
+	<select class="form-control" id="input-{$name}" name="$name">
+
+EOD;
+			foreach ($options as $opt_name => $opt_label) {
+				if ($opt_name != $default_value) {
+					$html .= <<<EOD
+			<option value="$opt_name">$opt_label</option>
+
+EOD;
+				} else {
+					$html .= <<<EOD
+			<option value="$opt_name" selected="selected">$opt_label</option>
+
+EOD;
+				}
+			}
+			$html .= <<<EOD
+	</select>
+</div>
+EOD;
+			$this->add($name, $html,
+				function($opt) use ($options) {
+					return isset($options[$opt]) ? '' : "无效选项";
+				},
+				null
+			);
+		}
+		
+		public function addTextArea($name, $label_text, $default_value, $validator_php, $validator_js) {
+			$default_value = htmlspecialchars($default_value);
+			$this->is_big = true;
+			$html = <<<EOD
+<div id="div-$name" class="form-group">
+	<label for="input-$name" class="col-sm-2 control-label">$label_text</label>
+	<div class="col-sm-10">
+		<textarea class="form-control" name="$name" id="input-$name">$default_value</textarea>
+		<span class="help-block" id="help-$name"></span>
+	</div>
+</div>
+EOD;
+			$this->add($name, $html, $validator_php, $validator_js);
+		}
+		public function addVTextArea($name, $label_text, $default_value, $validator_php, $validator_js) {
+			$default_value = htmlspecialchars($default_value);
+			$this->is_big = true;
+			$html = <<<EOD
+<div id="div-$name" class="form-group">
+	<label for="input-$name" class="control-label">$label_text</label>
+	<textarea class="form-control" name="$name" id="input-$name">$default_value</textarea>
+	<span class="help-block" id="help-$name"></span>
+</div>
+EOD;
+			$this->add($name, $html, $validator_php, $validator_js);
+		}
+		public function addCheckBox($name, $label_text, $default_value) {
+			$default_value = htmlspecialchars($default_value);
+			$status = $default_value ? 'checked="checked" ' : '';
+			$html = <<<EOD
+<div class="checkbox">
+	<label for="input-$name"><input type="checkbox" id="input-$name" name="$name" $status/> $label_text</label>
+</div>
+EOD;
+			$this->addNoVal($name, $html);
+		}
+		public function addCKEditor($name, $label_text, $default_value, $validator_php, $validator_js) {
+			$default_value = htmlspecialchars($default_value);
+			global $REQUIRE_LIB;
+			$REQUIRE_LIB['ckeditor'] = '';
+			
+			$this->is_big = true;
+			
+			$html = <<<EOD
+<div id="div-$name">
+	<label for="input-$name" class="control-label">$label_text</label>
+	<textarea class="ckeditor" name="$name" id="input-$name">$default_value</textarea>
+	<span class="help-block" id="help-$name"></span>
+</div>
+EOD;
+			$this->add($name, $html, $validator_php, $validator_js);
+		}
+		
+		public function addBlogEditor(UOJBlogEditor $editor) {
+			global $REQUIRE_LIB;
+			$REQUIRE_LIB['blog-editor'] = '';
+			
+			$this->is_big = true;
+			
+			$name = $editor->name;
+			
+			$this->addVInput("{$name}_title", 'text', '标题', $editor->cur_data['title'],
+				function ($title) use ($editor) {
+					return $editor->validateTitle();
+				},
+				null
+			);
+			
+			$content_md_html = HTML::div_vtextarea("{$name}_content_md", '内容', $editor->cur_data['content_md']);
+			
+			$this->add("{$name}_content_md", $content_md_html,
+				function ($content_md) use ($editor) {
+					return $editor->validateContentMd();
+				},
+				'always_ok'
+			);
+		
+			$this->appendHTML(<<<EOD
+<script type="text/javascript">blog_editor_init("$name");</script>
+EOD
+			);
+			
+			$this->run_at_server_handler["save-{$name}"] = function() use ($name, $editor) {
+				if ($this->no_submit) {
+					become404Page();
+				}
+				foreach (array("{$name}_title", "{$name}_content_md") as $field_name) {
+					if (!isset($_POST[$field_name])) {
+						becomeMsgPage('The form is incomplete.');
+					}
+				}
+				crsf_defend();
+				$errors = $this->validateAtServer();
+				if (!$errors) {
+					$editor->handleSave();
+				}
+				die(json_encode($errors));
+			};
+		}
+		public function addSlideEditor($name, $label_text, $default_value, $validator_php, $validator_js) {
+			$default_value = htmlspecialchars($default_value);
+			global $REQUIRE_LIB;
+			$REQUIRE_LIB['slide-editor'] = '';
+			
+			$this->is_big = true;
+			
+			$html = <<<EOD
+<div id="div-$name">
+	<label for="input-$name" class="control-label">$label_text</label>
+	<textarea name="$name" id="input-$name">$default_value</textarea>
+	<span class="help-block" id="help-$name"></span>
+</div>
+<script type="text/javascript">
+$('#input-$name').slide_editor();
+</script>
+EOD;
+			$this->add($name, $html, $validator_php, $validator_js);
+		}
+
+		static public function uploadedFileTmpName($name) {
+			if (isset($_FILES[$name]) && is_uploaded_file($_FILES[$name]['tmp_name'])) {
+				return $_FILES[$name]['tmp_name'];
+			} else {
+				return null;
+			}
+		}
+		
+		public function addSourceCodeInput($name, $text, $languages) {
+			$this->add("{$name}_upload_type", '',
+				function($type, &$vdata) use ($name) {
+					if ($type == 'editor') {
+						if (!isset($_POST["{$name}_editor"])) {
+							return '你在干啥……怎么什么都没交过来……?';
+						}
+					} elseif ($type == 'file') {
+					} else {
+						return '……居然既不是用编辑器上传也不是用文件上传的……= =……';
+					}
+				},
+				'always_ok'
+			);
+			$this->addNoVal("{$name}_editor", '');
+			$this->addNoVal("{$name}_file", '');
+			$this->add("{$name}_language", '',
+				function($lang) use ($languages) {
+					if (!in_array($lang, $languages)) {
+						return '该语言不被支持';
+					}
+					return '';
+				},
+				'always_ok'
+			);
+
+			$preferred_language = Cookie::get('uoj_preferred_language');
+			if ($preferred_language == null || !in_array($preferred_language, $languages)) {
+				$preferred_language = $languages[0];
+			}
+			
+			$langs_options_str = '';
+			foreach ($languages as $lang) {
+				$langs_options_str .= "<option";
+				if ($lang == $preferred_language) {
+					$langs_options_str .= ' selected="selected"';
+				}
+				$langs_options_str .= ">$lang</option>";
+			}
+			$langs_options_json = json_encode($langs_options_str);
+			$this->appendHTML(<<<EOD
+<div class="form-group" id="form-group-$name"></div>
+<script type="text/javascript">
+$('#form-group-$name').source_code_form_group('$name', '$text', $langs_options_json);
+</script>
+EOD
+			);
+			
+			$this->is_big = true;
+			$this->has_file = true;
+		}
+		public function addTextFileInput($name, $text) {
+			$this->add("{$name}_upload_type", '',
+				function($type, &$vdata) use ($name) {
+					if ($type == 'editor') {
+						if (!isset($_POST["{$name}_editor"])) {
+							return '你在干啥……怎么什么都没交过来……?';
+						}
+					} elseif ($type == 'file') {
+					} else {
+						return '……居然既不是用编辑器上传也不是用文件上传的……= =……';
+					}
+				},
+				'always_ok'
+			);
+			$this->addNoVal("{$name}_editor", '');
+			$this->addNoVal("{$name}_file", '');
+
+			$this->appendHTML(<<<EOD
+<div class="form-group" id="form-group-$name"></div>
+<script type="text/javascript">
+$('#form-group-$name').text_file_form_group('$name', '$text');
+</script>
+EOD
+		);
+			
+			$this->is_big = true;
+			$this->has_file = true;
+		}
+		
+		public function printHTML() {
+			$form_entype_str = $this->is_big ? ' enctype="multipart/form-data"' : '';
+			echo '<form action="', $_SERVER['REQUEST_URI'], '" method="post" class="form-horizontal" id="form-', $this->form_name, '"', $form_entype_str, '>';
+			echo HTML::hiddenToken();
+			echo $this->main_html;
+			
+			if (!$this->no_submit) {
+				if (!isset($this->submit_button_config['align'])) {
+					$this->submit_button_config['align'] = 'center';
+				}
+				if (!isset($this->submit_button_config['text'])) {
+					$this->submit_button_config['text'] = UOJLocale::get('submit');
+				}
+				if (!isset($this->submit_button_config['class_str'])) {
+					$this->submit_button_config['class_str'] = 'btn btn-secondary';
+				}
+				if ($this->submit_button_config['align'] == 'offset') {
+					echo '<div class="form-group">';
+					echo '<div class="col-sm-offset-2 col-sm-3">';
+				} else {
+					echo '<div class="text-', $this->submit_button_config['align'], '">';
+				}
+				echo '<button type="submit" id="button-submit-', $this->form_name, '" name="submit-', $this->form_name, '" value="', $this->form_name, '" class="mt-2 ', $this->submit_button_config['class_str'], '">', $this->submit_button_config['text'], '</button>';
+				if ($this->submit_button_config['align'] == 'offset') {
+					echo '</div>';
+				}
+				echo '</div>';
+			}
+			
+			echo '</form>';
+			
+			if ($this->no_submit) {
+				return;
+			}
+			
+			echo <<<EOD
+<script type="text/javascript">
+$(document).ready(function() {
+
+EOD;
+			if ($this->ctrl_enter_submit) {
+				echo <<<EOD
+	$('#form-{$this->form_name}').keydown(function(e) {
+		if (e.keyCode == 13 && e.ctrlKey) {
+			$('#button-submit-{$this->form_name}').click();
+		}
+	});
+
+EOD;
+			}
+			echo <<<EOD
+	$('#form-{$this->form_name}').submit(function(e) {
+		var ok = true;
+
+EOD;
+			$need_ajax = false;
+			if ($this->extra_validator) {
+				$need_ajax = true;
+			}
+			foreach ($this->data as $field) {
+				if ($field['validator_js'] != null) {
+					if ($field['validator_js'] != 'always_ok') {
+						echo <<<EOD
+		var ${field['name']}_err = (${field['validator_js']})($('#input-${field['name']}').val());
+
+EOD;
+					}
+				} else {
+					$need_ajax = true;
+				}
+			}
+			
+			if ($need_ajax) {
+				echo <<<EOD
+		var post_data = {};
+
+EOD;
+				foreach ($this->data as $field) {
+					if ($field['validator_js'] == null) {
+						echo <<<EOD
+		var ${field['name']}_err = 'Unknown error';
+		post_data.${field['name']} = $('#input-${field['name']}').val();
+
+EOD;
+					}
+				}
+				echo <<<EOD
+		if (post_data != {}) {
+			post_data['check-{$this->form_name}'] = "";
+			$.ajax({
+				url : '${_SERVER['REQUEST_URI']}',
+				type : 'POST',
+				dataType : 'json',
+				async : false,
+
+				data : post_data,
+				success : function(data) {
+
+EOD;
+				foreach ($this->data as $field) {
+					if ($field['validator_js'] == null) {
+						echo <<<EOD
+					${field['name']}_err = data.${field['name']};
+
+EOD;
+					}
+				}
+				echo <<<EOD
+					if (data.extra != undefined) {
+						alert(data.extra);
+						ok = false;
+					}
+
+EOD;
+				echo <<<EOD
+				}
+			});
+		}
+
+EOD;
+			}
+		
+			foreach ($this->data as $field) {
+				if ($field['validator_js'] != 'always_ok') {
+					echo <<<EOD
+		if (${field['name']}_err) {
+			$('#div-${field['name']}').addClass('has-error');
+			$('#help-${field['name']}').text(${field['name']}_err);
+			ok = false;
+		} else {
+			$('#div-${field['name']}').removeClass('has-error');
+			$('#help-${field['name']}').text('');
+		}
+
+EOD;
+				}
+			}
+			
+			if (isset($this->submit_button_config['smart_confirm'])) {
+				$this->submit_button_config['confirm_text'] = '你真的要' . $this->submit_button_config['text'] . '吗?';
+			}
+			if (isset($this->submit_button_config['confirm_text'])) {
+				echo <<<EOD
+		if (!confirm('{$this->submit_button_config['confirm_text']}')) {
+			ok = false;
+		}
+
+EOD;
+			}
+			if ($this->has_file) {
+				echo <<<EOD
+		$(this).find("input[type='file']").each(function() {
+			for (var i = 0; i < this.files.length; i++) {
+				if (this.files[i].size > 10 * 1024 * 1024) {
+					$('#div-' + $(this).attr('name')).addClass('has-error');
+					$('#help-' + $(this).attr('name')).text('文件大小不能超过10M');
+					ok = false;
+				} else {
+					$('#div-' + $(this).attr('name')).removeClass('has-error');
+					$('#help-' + $(this).attr('name')).text('');
+				}
+			}
+		});
+
+EOD;
+			}
+
+			if ($this->ajax_submit_js !== null) {
+				echo <<<EOD
+		e.preventDefault();
+		if (ok) {
+			$(this).ajaxSubmit({
+				beforeSubmit: function(formData) {
+					formData.push({name: 'submit-{$this->form_name}', value: '{$this->form_name}', type: 'submit'});
+				},
+				success : {$this->ajax_submit_js}
+			});
+		}
+
+EOD;
+			} else {
+				echo <<<EOD
+		return ok;
+
+EOD;
+			}
+			echo <<<EOD
+	});
+});
+</script>
+EOD;
+		}
+		
+		private function validateAtServer() {
+			$errors = array();
+			if ($this->extra_validator) {
+				$fun = $this->extra_validator;
+				$err = $fun();
+				if ($err) {
+					$errors['extra'] = $err;
+				}
+			}
+			foreach ($this->data as $field) {
+				if (!isset($field['no_val']) && isset($_POST[$field['name']])) {
+					$fun = $field['validator_php'];
+					$err = $fun($_POST[$field['name']], $this->vdata);
+					if ($err) {
+						$errors[$field['name']] = $err;
+					}
+				}
+			}
+			return $errors;
+		}
+		
+		public function runAtServer() {
+			foreach ($this->run_at_server_handler as $type => $handler) {
+				if (isset($_POST[$type])) {
+					$handler();
+				}
+			}
+		}
+	}
+	
+	function newAddDelCmdForm($form_name, $validate, $handle, $final = null) {
+		$form = new UOJForm($form_name);
+		$form->addTextArea(
+			$form_name . '_cmds', '命令', '',
+			function($str, &$vdata) use ($validate) {
+				$cmds = array();
+				foreach (explode("\n", $str) as $line_id => $raw_line) {
+					$line = trim($raw_line);
+					if ($line == '') {
+						continue;
+					}
+					if ($line[0] != '+' && $line[0] != '-') {
+						return '第' . ($line_id + 1) . '行:格式错误';
+					}
+					$obj = trim(substr($line, 1));
+					
+					if ($err = $validate($obj)) {
+						return '第' . ($line_id + 1) . '行:' . $err;
+					}
+					$cmds[] = array('type' => $line[0], 'obj' => $obj);
+				}
+				$vdata['cmds'] = $cmds;
+				return '';
+			},
+			null
+		);
+		if (!isset($final)) {
+			$form->handle = function(&$vdata) use ($handle) {
+				foreach ($vdata['cmds'] as $cmd) {
+					$handle($cmd['type'], $cmd['obj']);
+				}
+			};
+		} else {
+			$form->handle = function(&$vdata) use ($handle, $final) {
+				foreach ($vdata['cmds'] as $cmd) {
+					$handle($cmd['type'], $cmd['obj']);
+				}
+				$final();
+			};
+		}
+		return $form;
+	}
+	
+	function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) {
+		$form = new UOJForm($form_name);
+		foreach ($requirement as $req) {
+			if ($req['type'] == "source code") {
+				$languages = isset($req['languages']) ? $req['languages'] : $GLOBALS['uojSupportedLanguages'];
+				$form->addSourceCodeInput("{$form_name}_{$req['name']}", UOJLocale::get('problems::source code').':'.$req['name'], $languages);
+			} elseif ($req['type'] == "text") {
+				$form->addTextFileInput("{$form_name}_{$req['name']}", UOJLocale::get('problems::text file').':'.$req['file_name']);
+			}
+		}
+		
+		$form->handle = function(&$vdata) use ($form_name, $requirement, $zip_file_name_gen, $handle) {
+			global $myUser;
+			
+			if ($myUser == null) {
+				redirectToLogin();
+			}
+			
+			$tot_size = 0;
+			$zip_file_name = $zip_file_name_gen();
+			
+			$zip_file = new ZipArchive();
+			if ($zip_file->open(UOJContext::storagePath().$zip_file_name, ZipArchive::CREATE) !== true) {
+				becomeMsgPage('提交失败');
+			}
+			
+			$content = array();
+			$content['file_name'] = $zip_file_name;
+			$content['config'] = array();
+			foreach ($requirement as $req) {
+				if ($req['type'] == "source code") {
+					$content['config'][] = array("{$req['name']}_language", $_POST["{$form_name}_{$req['name']}_language"]);
+				}
+			}
+			
+			foreach ($requirement as $req) {
+				if ($_POST["{$form_name}_{$req['name']}_upload_type"] == 'editor') {
+					$zip_file->addFromString($req['file_name'], $_POST["{$form_name}_{$req['name']}_editor"]);
+				} else {
+					$tmp_name = UOJForm::uploadedFileTmpName("{$form_name}_{$req['name']}_file");
+					if ($tmp_name == null) {
+						$zip_file->addFromString($req['file_name'], '');
+					} else {
+						$zip_file->addFile($tmp_name, $req['file_name']);
+					}
+				}
+				$stat = $zip_file->statName($req['file_name']);
+				
+				if ($req['type'] == 'source code') {
+					$max_size = isset($req['size']) ? (int)$req['size'] : 50;
+					if ($stat['size'] > $max_size * 1024) {
+						$zip_file->close();
+						unlink(UOJContext::storagePath().$zip_file_name);
+						becomeMsgPage("源代码长度不能超过 {$max_size}KB。");
+					}
+				}
+				
+				$tot_size += $stat['size'];
+			}
+			
+			$zip_file->close();
+			
+			$handle($zip_file_name, $content, $tot_size);
+		};
+		return $form;
+	}
+	function newZipSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) {
+		$form = new UOJForm($form_name);
+		$name = "zip_ans_{$form_name}";
+		$text = UOJLocale::get('problems::zip file upload introduction', join(array_map(function($req) {
+			return $req['file_name'];
+		}, $requirement), ', '));
+		$browse_text = UOJLocale::get('browse');
+		$html = <<<EOD
+<div id="div-{$name}">
+	<label for="input-{$name}">$text</label>
+	<input type="file" id="input-{$name}" name="{$name}" style="display:none;" onchange="$('#input-{$name}_path').val($('#input-{$name}').val());" />
+	<div class="input-group bot-buffer-md">
+		<input id="input-{$name}_path" class="form-control" type="text" readonly="readonly" />
+		<span class="input-group-append">
+			<button type="button" class="btn btn-primary" style="width:100px; !important" onclick="$('#input-{$name}').click();"><span class="glyphicon glyphicon-folder-open"></span> $browse_text</button>
+		</span>
+	</div>
+	<span class="help-block" id="help-{$name}"></span>
+</div>
+EOD;
+		
+		$form->addNoVal($name, $html);	
+		$form->is_big = true;
+		$form->has_file = true;
+		
+		$form->handle = function() use ($name, $requirement, $zip_file_name_gen, $handle) {
+			global $myUser;
+			
+			if ($myUser == null) {
+				redirectToLogin();
+			}
+			
+			if (!isset($_FILES[$name])) {
+				becomeMsgPage('你在干啥……怎么什么都没交过来……?');
+			} elseif (!is_uploaded_file($_FILES[$name]['tmp_name'])) {
+				becomeMsgPage('上传出错,貌似你什么都没交过来……?');
+			}
+			
+			$up_zip_file = new ZipArchive();
+			if ($up_zip_file->open($_FILES[$name]['tmp_name']) !== true) {
+				becomeMsgPage('不是合法的zip压缩文件');
+			}
+			
+			$tot_size = 0;
+			$zip_content = array();
+			foreach ($requirement as $req) {
+				$stat = $up_zip_file->statName($req['file_name']);
+				if ($stat === false) {
+					$zip_content[$req['name']] = '';
+				} else {
+					$tot_size += $stat['size'];
+					if ($stat['size'] > 20 * 1024 * 1024) {
+						becomeMsgPage("文件 {$req['file_name']} 实际大小过大。");
+					}
+					$ret = $up_zip_file->getFromName($req['file_name']);
+					if ($ret === false) {
+						$zip_content[$req['name']] = '';
+					} else {
+						$zip_content[$req['name']] = $ret;
+					}
+				}
+			}
+			$up_zip_file->close();
+			
+			$zip_file_name = $zip_file_name_gen();
+			
+			$zip_file = new ZipArchive();
+			if ($zip_file->open(UOJContext::storagePath().$zip_file_name, ZipArchive::CREATE) !== true) {
+				becomeMsgPage('提交失败');
+			}
+			
+			foreach ($requirement as $req) {
+				$zip_file->addFromString($req['file_name'], $zip_content[$req['name']]);
+			}
+			$zip_file->close();
+			
+			$content = array();
+			$content['file_name'] = $zip_file_name;
+			$content['config'] = array();
+			
+			$handle($zip_file_name, $content, $tot_size);
+		};
+		return $form;
+	}
+?>

+ 1330 - 0
app/libs/uoj-html-lib.php

@@ -0,0 +1,1330 @@
+<?php
+
+function uojHandleAtSign($str, $uri) {
+	$referrers = array();
+	$res = preg_replace_callback('/@(@|[a-zA-Z0-9_]{1,20})/', function($matches) use (&$referrers) {
+		if ($matches[1] === '@') {
+			return '@';
+		} else {
+			$user = queryUser($matches[1]);
+			if ($user == null) {
+				return $matches[0];
+			} else {
+				$referrers[$user['username']] = '';
+				return '<span class="uoj-username" data-rating="'.$user['rating'].'">@'.$user['username'].'</span>';
+			}
+		}
+	}, $str);
+	
+	$referrers_list = array();
+	foreach ($referrers as $referrer => $val) {
+		$referrers_list[] = $referrer;
+	}
+	
+	return array($res, $referrers_list);
+}
+
+function uojFilePreview($file_name, $output_limit, $file_type = 'text') {
+	switch ($file_type) {
+		case 'text':
+			return strOmit(file_get_contents($file_name, false, null, 0, $output_limit + 4), $output_limit);
+		default:
+			return strOmit(shell_exec('xxd -g 4 -l 5000 ' . escapeshellarg($file_name) . ' | head -c ' . ($output_limit + 4)), $output_limit);
+	}
+}
+
+function uojIncludeView($name, $view_params = array()) {
+	extract($view_params);
+	include $_SERVER['DOCUMENT_ROOT'].'/app/views/'.$name.'.php';
+}
+
+function redirectTo($url) {
+	header('Location: '.$url);
+	die();
+}
+function permanentlyRedirectTo($url) {
+	header("HTTP/1.1 301 Moved Permanently"); 
+	header('Location: '.$url);
+	die();
+}
+function redirectToLogin() {
+	if (UOJContext::isAjax()) {
+		die('please <a href="'.HTML::url('/login').'">login</a>');
+	} else {
+		header('Location: '.HTML::url('/login'));
+		die();
+	}
+}
+function becomeMsgPage($msg, $title = '消息') {
+	if (UOJContext::isAjax()) {
+		die($msg);
+	} else {
+		echoUOJPageHeader($title);
+		echo $msg;
+		echoUOJPageFooter();
+		die();
+	}
+}
+function become404Page() {
+	header($_SERVER['SERVER_PROTOCOL'] . " 404 Not Found", true, 404);
+	becomeMsgPage('<div class="text-center"><div style="font-size:233px">404</div><p>唔……未找到该页面……你是从哪里点进来的……&gt;_&lt;……</p></div>', '404');
+}
+function become403Page() {
+	header($_SERVER['SERVER_PROTOCOL'] . " 403 Forbidden", true, 403); 
+	becomeMsgPage('<div class="text-center"><div style="font-size:233px">403</div><p>禁止入内! T_T</p></div>', '403');
+}
+
+function getUserLink($username, $rating = null, $certs = null) {
+	if (validateUsername($username) && ($user = queryUser($username))) {
+		if ($rating == null) {
+			$rating = $user['rating'];
+		}
+		if ($cert == null) {
+			$certs = $user['certs'];
+		}
+		return '<span class="uoj-username" data-rating="'.$rating.'" data-certs="'.$certs.'">'.$username.'</span>';
+	} else {
+		$esc_username = HTML::escape($username);
+		return '<span>'.$esc_username.'</span>';
+	}
+}
+
+function getRemoteProblemLink($rpid) {
+	$rp = queryRemoteProblemBrief($rpid);
+	return '<a href="/rp/' . $rpid . '">' . $rp['title'] . '</a>';
+}
+
+function getProblemLink($problem, $problem_title = '!title_only') {
+	if ($problem_title == '!title_only') {
+		$problem_title = $problem['title'];
+	} elseif ($problem_title == '!id_and_title') {
+		$problem_title = "#${problem['id']}. ${problem['title']}";
+	}
+	return '<a href="/problem/'.$problem['id'].'">'.$problem_title.'</a>';
+}
+function getContestProblemLink($problem, $contest_id, $problem_title = '!title_only') {
+	if ($problem_title == '!title_only') {
+		$problem_title = $problem['title'];
+	} elseif ($problem_title == '!id_and_title') {
+		$problem_title = "#{$problem['id']}. {$problem['title']}";
+	}
+	return '<a href="/contest/'.$contest_id.'/problem/'.$problem['id'].'">'.$problem_title.'</a>';
+}
+function getBlogLink($id) {
+	if (validateUInt($id) && $blog = queryBlog($id)) {
+		return '<a href="/blogs/'.$id.'">'.$blog['title'].'</a>';
+	}
+}
+function getClickZanBlock($type, $id, $cnt, $val = null) {
+	if ($val == null) {
+		$val = queryZanVal($id, $type, Auth::user());
+	}
+	return '<div class="uoj-click-zan-block" data-id="'.$id.'" data-type="'.$type.'" data-val="'.$val.'" data-cnt="'.$cnt.'"></div>';
+}
+
+
+function getLongTablePageRawUri($page) {
+	$path = strtok(UOJContext::requestURI(), '?');
+	$query_string = strtok('?');
+	parse_str($query_string, $param);
+			
+	$param['page'] = $page;
+	if ($page == 1) {
+		unset($param['page']);
+	}
+			
+	if ($param) {
+		return $path . '?' . http_build_query($param);
+	} else {
+		return $path;
+	}
+}
+function getLongTablePageUri($page) {
+	return HTML::escape(getLongTablePageRawUri($page));
+}
+
+function echoLongTable($col_names, $table_name, $cond, $tail, $header_row, $print_row, $config) {
+	$pag_config = $config;
+	$pag_config['col_names'] = $col_names;
+	$pag_config['table_name'] = $table_name;
+	$pag_config['cond'] = $cond;
+	$pag_config['tail'] = $tail;
+	$pag = new Paginator($pag_config);
+
+	$div_classes = isset($config['div_classes']) ? $config['div_classes'] : array('table-responsive', 'shadow', 'my-4');
+	$table_classes = isset($config['table_classes']) ? $config['table_classes'] : array('table', 'table-bordered', 'table-hover', 'table-striped', 'table-text-center');
+		
+	echo '<div class="', join($div_classes, ' '), '">';
+	echo '<table class="', join($table_classes, ' '), '">';
+	echo '<thead>';
+	echo $header_row;
+	echo '</thead>';
+	echo '<tbody>';
+
+	foreach ($pag->get() as $idx => $row) {
+		if (isset($config['get_row_index'])) {
+			$print_row($row, $idx);
+		} else {
+			$print_row($row);
+		}
+	}
+	if ($pag->isEmpty()) {
+		echo '<tr><td colspan="233">'.UOJLocale::get('none').'</td></tr>';
+	}
+
+	echo '</tbody>';
+	echo '</table>';
+	echo '</div>';
+	
+	if (isset($config['print_after_table'])) {
+		$fun = $config['print_after_table'];
+		$fun();
+	}
+		
+	echo $pag->pagination();
+}
+
+function getSubmissionStatusDetails($submission) {
+	$html = '<td colspan="233" style="vertical-align: middle">';
+	
+	$out_status = explode(', ', $submission['status'])[0];
+	
+	$fly = '<img src="/images/utility/qpx_n/b37.gif" alt="小熊像超人一样飞" class="img-rounded" />';
+	$think = '<img src="/images/utility/qpx_n/b29.gif" alt="小熊像在思考" class="img-rounded" />';
+	
+	if ($out_status == 'Judged') {
+		$status_text = '<strong>Judged!</strong>';
+		$status_img = $fly;
+	} else {
+		if ($submission['status_details'] !== '') {
+			$status_img = $fly;
+			$status_text = HTML::escape($submission['status_details']);
+		} else {
+			$status_img = $think;
+			$status_text = $out_status;
+		}
+	}
+	$html .= '<div class="uoj-status-details-img-div">' . $status_img . '</div>';
+	$html .= '<div class="uoj-status-details-text-div">' . $status_text . '</div>';
+
+	$html .= '</td>';
+	return $html;
+}
+
+function echoSubmission($submission, $config, $user) {
+	$problem = queryProblemBrief($submission['problem_id']);
+	$submitterLink = getUserLink($submission['submitter']);
+	
+	if ($submission['score'] == null) {
+		$used_time_str = "/";
+		$used_memory_str = "/";
+	} else {
+		$used_time_str = $submission['used_time'] . 'ms';
+		$used_memory_str = $submission['used_memory'] . 'kb';
+	}
+	
+	$status = explode(', ', $submission['status'])[0];
+	
+	$show_status_details = Auth::check() && ($submission['submitter'] === Auth::id() || isSuperUser(queryUser(Auth::id()))) && $status !== 'Judged';
+	
+	if (!$show_status_details) {
+		echo '<tr>';
+	} else {
+		echo '<tr class="warning">';
+	}
+	if (!isset($config['id_hidden'])) {
+		echo '<td><a href="/submission/', $submission['id'], '">#', $submission['id'], '</a></td>';
+	}
+	if (!isset($config['problem_hidden'])) {
+		if ($submission['contest_id']) {
+			echo '<td>', getContestProblemLink($problem, $submission['contest_id'], '!id_and_title'), '</td>';
+		} else if ($submission['problem_id'] != 260328) {
+			echo '<td>', getProblemLink($problem, '!id_and_title'), '</td>';
+		} else {
+			foreach (json_decode($submission['content'], true)['config'] as $row)
+				if ($row[0] == 'rpid') {
+					$rpid = $row[1];
+					break;
+				}
+			echo '<td>' . getRemoteProblemLink($rpid) . '</td>';
+		}
+	}
+	if (!isset($config['submitter_hidden'])) {
+		echo '<td>', $submitterLink, '</td>';
+	}
+	if (!isset($config['result_hidden'])) {
+		echo '<td>';
+		if ($status == 'Judged') {
+			if ($submission['score'] == null) {
+				echo '<a href="/submission/', $submission['id'], '" class="small">', $submission['result_error'], '</a>';
+			} else {
+				echo '<a href="/submission/', $submission['id'], '" class="uoj-score">', $submission['score'], '</a>';
+			}
+		} else {
+			echo '<a href="/submission/', $submission['id'], '" class="small">', $status, '</a>';
+		}
+		echo '</td>';
+	}
+	if (!isset($config['used_time_hidden'])) {
+		echo '<td>', $used_time_str, '</td>';
+	}
+	if (!isset($config['used_memory_hidden'])) {
+		echo '<td>', $used_memory_str, '</td>';
+	}
+
+	echo '<td>', '<a href="/submission/', $submission['id'], '">', $submission['language'], '</a>', '</td>';
+
+	if ($submission['tot_size'] < 1024) {
+		$size_str = $submission['tot_size'] . 'b';
+	} else {
+		$size_str = sprintf("%.1f", $submission['tot_size'] / 1024) . 'kb';
+	}
+	echo '<td>', $size_str, '</td>';
+
+	if (!isset($config['submit_time_hidden'])) {
+		echo '<td><small>', $submission['submit_time'], '</small></td>';
+	}
+	if (!isset($config['judge_time_hidden'])) {
+		echo '<td><small>', $submission['judge_time'], '</small></td>';
+	}
+	echo '</tr>';
+	if ($show_status_details) {
+		echo '<tr id="', "status_details_{$submission['id']}", '" class="info">';
+		echo getSubmissionStatusDetails($submission);
+		echo '</tr>';
+		echo '<script type="text/javascript">update_judgement_status_details('.$submission['id'].')</script>';
+	}
+}
+
+
+function echoSubmissionsListOnlyOne($submission, $config, $user) {
+	echo '<div class="table-responsive shadow my-4">';
+	echo '<table class="table table-bordered table-text-center">';
+	echo '<thead>';
+	echo '<tr>';
+	if (!isset($config['id_hidden'])) {
+		echo '<th>ID</th>';
+	}
+	if (!isset($config['problem_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::problem').'</th>';
+	}
+	if (!isset($config['submitter_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::submitter').'</th>';
+	}
+	if (!isset($config['result_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::result').'</th>';
+	}
+	if (!isset($config['used_time_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::used time').'</th>';
+	}
+	if (!isset($config['used_memory_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::used memory').'</th>';
+	}
+	echo '<th>'.UOJLocale::get('problems::language').'</th>';
+	echo '<th>'.UOJLocale::get('problems::file size').'</th>';
+	if (!isset($config['submit_time_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::submit time').'</th>';
+	}
+	if (!isset($config['judge_time_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::judge time').'</th>';
+	}
+	echo '</tr>';
+	echo '</thead>';
+	echo '<tbody>';
+	echoSubmission($submission, $config, $user);
+	echo '</tbody>';
+	echo '</table>';
+	echo '</div>';
+}
+
+
+function echoSubmissionsList($cond, $tail, $config, $user) {
+	$header_row = '<tr>';
+	$col_names = array();
+	$col_names[] = 'submissions.status_details';
+	$col_names[] = 'submissions.status';
+	$col_names[] = 'submissions.result_error';
+	$col_names[] = 'submissions.score';
+	$col_names[] = 'submissions.content';
+	
+	if (!isset($config['id_hidden'])) {
+		$header_row .= '<th>ID</th>';
+		$col_names[] = 'submissions.id';
+	}
+	if (!isset($config['problem_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::problem').'</th>';
+		$col_names[] = 'submissions.problem_id';
+		$col_names[] = 'submissions.contest_id';
+	}
+	if (!isset($config['submitter_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::submitter').'</th>';
+		$col_names[] = 'submissions.submitter';
+	}
+	if (!isset($config['result_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::result').'</th>';
+	}
+	if (!isset($config['used_time_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::used time').'</th>';
+		$col_names[] = 'submissions.used_time';
+	}
+	if (!isset($config['used_memory_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::used memory').'</th>';
+		$col_names[] = 'submissions.used_memory';
+	}
+	$header_row .= '<th>'.UOJLocale::get('problems::language').'</th>';
+	$col_names[] = 'submissions.language';
+	$header_row .= '<th>'.UOJLocale::get('problems::file size').'</th>';
+	$col_names[] = 'submissions.tot_size';
+
+	if (!isset($config['submit_time_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::submit time').'</th>';
+		$col_names[] = 'submissions.submit_time';
+	}
+	if (!isset($config['judge_time_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::judge time').'</th>';
+		$col_names[] = 'submissions.judge_time';
+	}
+	$header_row .= '</tr>';
+	
+	$table_name = isset($config['table_name']) ? $config['table_name'] : 'submissions';
+	
+	if (!isSuperUser($user)) {
+		if ($user != null) {
+			$permission_cond = "submissions.is_hidden = false or (submissions.is_hidden = true and submissions.problem_id in (select problem_id from problems_permissions where username = '{$user['username']}')) or (submissions.is_hidden = 1 and (select max(find_in_set(submissions.problem_id, privs)) as nr from user_info where username like 'group_%' and find_in_set(username, '{$user['extgroups']}')))";
+		} else {
+			$permission_cond = "submissions.is_hidden = false";
+		}
+		if ($cond !== '1') {
+			$cond = "($cond) and ($permission_cond)";
+		} else {
+			$cond = $permission_cond;
+		}
+	}
+	
+	$table_config = isset($config['table_config']) ? $config['table_config'] : null;
+	
+	echoLongTable($col_names, $table_name, $cond, $tail, $header_row,
+		function($submission) use ($config, $user) {
+			echoSubmission($submission, $config, $user);
+		}, $table_config);
+}
+
+function echoShortList($header, $rows, $print_row) {	
+	echo '<div class="table-responsive"><table class="table table-bordered table-hover table-striped table-text-center">';
+	echo '<thead>';
+	echo $header;
+	echo '</thead>';
+	echo '<tbody>';
+
+	foreach ($rows as $idx => $row)
+		$print_row($row);
+	if (empty($rows))
+		echo '<tr><td colspan="233">'.UOJLocale::get('none').'</td></tr>';
+	echo '</tbody>';
+	echo '</table>';
+	echo '</div>';
+}
+
+function echoBelongGroupList($user) {
+	echoShortList('<tr><th>组名</th><th>题目</th></tr>', queryUserExtgroups($user), function($grp) {
+		$link = getUserLink(queryUser($grp)['username']);
+		echo <<<HTML
+<tr>
+	<td>$link</td>
+	<td><a href="/problems/group/{$grp}">题目</a></td>
+</tr>
+HTML;
+	});
+}
+
+function echoGroupMemberList($grp) {
+	echoShortList('<tr><th>用户名</th><th>管理</th></tr>', queryGroupMembers($grp),
+		function($usern) use ($grp) {
+			$username = $usern['username'];
+			$user = queryUser($username);
+			$link = getUserLink($username);
+			if (hasGroupCtrlPermission($user, $grp)) $adminp = '<b>是</b>';
+			else $adminp = '否';
+			echo <<<HTML
+<tr>
+	<td>$link</td>
+<td>$adminp</td>
+</tr>
+HTML;
+		});
+}
+
+function echoGroupProblemList($grp) {
+	echoShortList('<tr><th>题号</th><th>题名</th><th>控制</th><th>压缩包大小</th></tr>', queryGroupProblems($grp),
+		function($pid) use ($grp) {
+			$problem = queryProblemBrief($pid);
+			$pid = $pid;
+			$title = $problem['title'];
+			if ($problem['ctrl'] && $problem['ctrl'] == $grp['username']) $adminp = '<b>是</b>';
+			else $adminp = '否';
+			$size = printSize(queryProblemSize($pid));
+			echo <<<HTML
+<tr><td>$pid</td>
+<td><a href="/problem/{$pid}">$title</a></td>
+<td>$adminp</td>
+<td>$size</td></tr>
+HTML;
+		});
+}
+
+function echoGroupProblemSizeList($grp) {
+	echoShortList('<tr><th>题号</th><th>题名</th><th>压缩包大小</th></tr>', queryGroupCtrlProblems($grp),
+		function($pid) {
+			$problem = queryProblemBrief($pid);
+			$pid = $pid;
+			$title = $problem['title'];
+			$size = printSize(queryProblemSize($pid));
+			echo <<<HTML
+<tr><td>$pid</td>
+<td><a href="/problem/{$pid}">$title</a></td>
+<td>$size</td></tr>
+HTML;
+		});
+}
+function echoControlGroupList($user) {
+	echoShortList('<tr><th>组名</th><th>管理</th></tr>', queryUserCtrlgroups($user), function($grp) {
+		$link = getUserLink(queryUser($grp)['username']);
+		echo <<<HTML
+<tr>
+	<td>$link</td>
+	<td><a href="/group-manage/{$grp}">管理</a></td>
+</tr>
+HTML;
+	});
+}
+
+function echoGroupsList() {
+	$header_row = '<tr>';
+	$col_names = ['username', 'privs'];
+	$header_row .= '<th>组名</th>';
+	$header_row .= '<th>组权限</th>';
+	$header_row .= '</tr>';
+	$table_name = 'user_info';
+	echoLongTable($col_names, $table_name, "username like 'group_%'", '', $header_row,
+		function ($group) {
+			$link = getUserLink($group['username']);
+			$privs = $group['privs'];
+			echo <<<HTML
+<tr>
+	<td>
+		{$link}
+	</td>
+	<td>
+		{$privs_pretty}
+	</td>
+</tr>
+HTML;
+		}, []);
+}
+
+function echoCertsList($user) {
+	echoShortList('<tr><th>勋章</th><th>展示</th></tr>', explode(',', $user['certslist']), function($certs) {
+		$col = explode('#', $certs)[0];
+		$text = explode('#', $certs)[1];
+		$cert = '<span class="uoj-certs uoj-certs-' . $col . '">' . $text . '</span>';
+		echo <<<HTML
+<tr>
+	<td>$cert</td>
+	<td>
+		<form action="/certs-manage" method="post">
+			<input type="hidden" name="certs" value="$certs">
+			<button type="submit" class="btn btn-primary">展示这个</button>
+		</form>
+	</td>
+</tr>
+HTML;
+	});
+}
+
+function echoPastesList() {
+	$header_row = '<tr>';
+	$col_names = ['`index`','creator','created_at'];
+	$header_row .= '<th>ID</th>';
+	$header_row .= '<th>'.UOJLocale::get("problems::submitter").'</th>';
+	$header_row .= '<th>'.UOJLocale::get('problems::submit time').'</th>';
+	$header_row .= '<th> 操作 </th>';
+	$header_row .= '</tr>';
+	$table_name = 'pastes';
+	echoLongTable($col_names, $table_name, "1", 'order by created_at desc', $header_row,
+		function($paste) {
+			$user = getUserLink($paste['creator']);
+			$token = HTML::hiddenToken();
+			echo <<<HTML
+<tr>
+	<td>
+		<a href="/pastes/{$paste['index']}">{$paste['index']}</a>
+	</td>
+	<td>
+		{$user}
+	</td>
+	<td>
+		{$paste['created_at']}
+	</td>
+	<td>
+		<form action="/super-manage/paste" method="post" class="form-horizontal">
+		{$token}
+		<input type="text" class="form-control" name="paste_deleter_name" id="input-paste_deleter_name" value="{$paste['index']}" style="display: none;">
+		<button type="submit" name="submit-paste_deleter" value="paste_deleter" class="btn btn-sm btn-danger" style="margin: 0">删除</button>
+		</form>
+	</td>
+</tr>
+HTML;
+		}, []);
+}
+
+function echoPasteContent($paste) {
+	$zip_file = new ZipArchive();
+	$submission_content = json_decode($paste['content'], true);
+	$zip_file->open(UOJContext::storagePath().$submission_content['file_name']);
+
+	$config = array();
+	foreach ($submission_content['config'] as $config_key => $config_val) {
+		$config[$config_val[0]] = $config_val[1];
+	}
+
+	$file_content = $zip_file->getFromName("paste.code");
+	$file_content = uojTextEncode($file_content, array('allow_CR' => true, 'html_escape' => true));
+	$file_language = htmlspecialchars($config["paste_language"]);
+	$footer_text = UOJLocale::get('problems::source code').', '.UOJLocale::get('problems::language').': '.$file_language;
+	$footer_text .= ", ".UOJLocale::get("problems::submitter") . <<<HTML
+: <a href="/user/profile/${paste['creator']}">${paste['creator']}</a>
+HTML;
+	$footer_text .= ", ".UOJLocale::get("problems::submit time").": ".$paste['created_at'];
+
+	switch ($file_language) {
+		case 'C++14':
+		case 'C++17':
+		case 'C++17ubsan':
+			$sh_class = 'sh_cpp';
+			break;
+		case 'Python2':
+		case 'Python3':
+			$sh_class = 'sh_python';
+			break;
+		case 'Java8':
+		case 'Java11':
+			$sh_class = 'sh_java';
+			break;
+		case 'C':
+			$sh_class = 'sh_c';
+			break;
+		case 'Pascal':
+			$sh_class = 'sh_pascal';
+			break;
+		default:
+			$sh_class = '';
+			break;
+	}
+	echo '<div class="card border-info mb-3">';
+	echo '<div class="card-header bg-info">';
+	echo '<h4 class="card-title">Paste!</h4>';
+	echo '</div>';
+	echo '<div class="card-body">';
+	echo '<pre><code class="'.$sh_class.'">'.$file_content."\n".'</code></pre>';
+	echo '</div>';
+	echo '<div class="card-footer">'.$footer_text.'</div>';
+	echo '</div>';
+
+	$zip_file->close();
+}
+
+function echoSubmissionContent($submission, $requirement) {
+	$zip_file = new ZipArchive();
+	$submission_content = json_decode($submission['content'], true);
+	$zip_file->open(UOJContext::storagePath().$submission_content['file_name']);
+	
+	$config = array();
+	foreach ($submission_content['config'] as $config_key => $config_val) {
+		$config[$config_val[0]] = $config_val[1];
+	}
+	
+	foreach ($requirement as $req) {
+		if ($req['type'] == "source code") {
+			$file_content = $zip_file->getFromName("{$req['name']}.code");
+			$file_content = uojTextEncode($file_content, array('allow_CR' => true, 'html_escape' => true));
+			$file_language = htmlspecialchars($config["{$req['name']}_language"]);
+			$footer_text = UOJLocale::get('problems::source code').', '.UOJLocale::get('problems::language').': '.$file_language;
+			switch ($file_language) {
+				case 'C++14':
+				case 'C++17':
+				case 'C++17ubsan':
+					$sh_class = 'sh_cpp';
+					break;
+				case 'Python2':
+				case 'Python3':
+					$sh_class = 'sh_python';
+					break;
+				case 'Java8':
+				case 'Java11':
+					$sh_class = 'sh_java';
+					break;
+				case 'C':
+					$sh_class = 'sh_c';
+					break;
+				case 'Pascal':
+					$sh_class = 'sh_pascal';
+					break;
+				default:
+					$sh_class = '';
+					break;
+			}
+			echo '<div class="card border-info mb-3">';
+			echo '<div class="card-header bg-info">';
+			echo '<h4 class="card-title">'.$req['name'].'</h4>';
+			echo '</div>';
+			echo '<div class="card-body">';
+			echo '<pre><code class="'.$sh_class.'">'.$file_content."\n".'</code></pre>';
+			echo '</div>';
+			echo '<div class="card-footer">'.$footer_text.'</div>';
+			echo '</div>';
+		} elseif ($req['type'] == "text") {
+			$file_content = $zip_file->getFromName("{$req['file_name']}", 504);
+			$file_content = strOmit($file_content, 500);
+			$file_content = uojTextEncode($file_content, array('allow_CR' => true, 'html_escape' => true));
+			$footer_text = UOJLocale::get('problems::text file');
+			echo '<div class="card border-info mb-3">';
+			echo '<div class="card-header bg-info">';
+			echo '<h4 class="card-title">'.$req['file_name'].'</h4>';
+			echo '</div>';
+			echo '<div class="card-body">';
+			echo '<pre>'."\n".$file_content."\n".'</pre>';
+			echo '</div>';
+			echo '<div class="card-footer">'.$footer_text.'</div>';
+			echo '</div>';
+		}
+	}
+
+	$zip_file->close();
+}
+
+
+class JudgementDetailsPrinter {
+	private $name;
+	private $styler;
+	private $dom;
+	
+	private $subtask_num;
+
+	private function _print_c($node) {
+		foreach ($node->childNodes as $child) {
+			if ($child->nodeName == '#text') {
+				echo htmlspecialchars($child->nodeValue);
+			} else {
+				$this->_print($child);
+			}
+		}
+	}
+	private function _print($node) {
+		if ($node->nodeName == 'error') {
+			echo "<pre>\n";
+			$this->_print_c($node);
+			echo "\n</pre>";
+		} elseif ($node->nodeName == 'tests') {
+			echo '<div id="', $this->name, '_details_accordion">';
+			if ($this->styler->show_small_tip) {
+				echo '<div class="text-right text-muted">', '小提示:点击横条可展开更详细的信息', '</div>';
+			} elseif ($this->styler->ioi_contest_is_running) {
+				echo '<div class="text-right text-muted">', 'IOI 赛制比赛中不支持显示详细信息', '</div>';
+			}
+			$this->_print_c($node);
+			echo '</div>';
+		} elseif ($node->nodeName == 'subtask') {
+			$subtask_num = $node->getAttribute('num');
+			$subtask_score = $node->getAttribute('score');
+			$subtask_info = $node->getAttribute('info');
+			
+			echo '<div class="card ', $this->styler->getTestInfoClass($subtask_info), ' mb-3">';
+			
+			$accordion_parent = "{$this->name}_details_accordion";
+			$accordion_collapse =  "{$accordion_parent}_collapse_subtask_{$subtask_num}";
+			$accordion_collapse_accordion =  "{$accordion_collapse}_accordion";
+			echo 	'<div class="card-header" data-toggle="collapse" data-parent="#', $accordion_parent, '" data-target="#', $accordion_collapse, '">';
+			
+			echo 		'<div class="row">';
+			echo 			'<div class="col-sm-4">';
+			echo 				'<h3 class="card-title">', 'Subtask #', $subtask_num, ': ', '</h3>';
+			echo 			'</div>';
+			
+			if ($this->styler->show_score) {
+				echo 		'<div class="col-sm-4">';
+				echo 			'得分:', $subtask_score;
+				echo 		'</div>';
+				echo 		'<div class="col-sm-4">';
+				echo 			htmlspecialchars($subtask_info);
+				echo 		'</div>';
+			} else {
+				echo 		'<div class="col-sm-8">';
+				echo 			htmlspecialchars($subtask_info);
+				echo 		'</div>';
+			}
+
+			echo 		'</div>';
+			echo 	'</div>';
+			
+			echo 	'<div id="', $accordion_collapse, '" class="card-collapse collapse">';
+			echo 		'<div class="card-body">';
+
+			echo 			'<div id="', $accordion_collapse_accordion, '">';
+			$this->subtask_num = $subtask_num;
+			$this->_print_c($node);
+			$this->subtask_num = null;
+			echo 			'</div>';
+
+			echo 		'</div>';
+			echo 	'</div>';
+			echo '</div>';
+		} elseif ($node->nodeName == 'test') {
+			$test_info = $node->getAttribute('info');
+			$test_num = $node->getAttribute('num');
+			$test_score = $node->getAttribute('score');
+			$test_time = $node->getAttribute('time');
+			$test_memory = $node->getAttribute('memory');
+
+			echo '<div class="card ', $this->styler->getTestInfoClass($test_info), ' mb-3">';
+			
+			$accordion_parent = "{$this->name}_details_accordion";
+			if ($this->subtask_num != null) {
+				$accordion_parent .= "_collapse_subtask_{$this->subtask_num}_accordion";
+			}
+			$accordion_collapse = "{$accordion_parent}_collapse_test_{$test_num}";
+			if (!$this->styler->shouldFadeDetails($test_info)) {
+				echo '<div class="card-header" data-toggle="collapse" data-parent="#', $accordion_parent, '" data-target="#', $accordion_collapse, '">';
+			} else {
+				echo '<div class="card-header">';
+			}
+			echo '<div class="row">';
+			echo '<div class="col-sm-4">';
+			if ($test_num > 0) {
+				echo '<h4 class="card-title">', 'Test #', $test_num, ': ', '</h4>';
+			} else {
+				echo '<h4 class="card-title">', 'Extra Test:', '</h4>';
+			}
+			echo '</div>';
+				
+			if ($this->styler->show_score) {
+				echo '<div class="col-sm-4">';
+				echo '得分:', $test_score;
+				echo '</div>';
+				echo '<div class="col-sm-4">';
+				echo htmlspecialchars($test_info);
+				echo '</div>';
+			} else {
+				echo '<div class="col-sm-8">';
+				echo htmlspecialchars($test_info);
+				echo '</div>';
+			}
+				
+			echo '<div class="col-sm-4">';
+			if ($test_time >= 0) {
+				echo '时间:', $test_time, 'ms';
+			}
+			echo '</div>';
+
+			echo '<div class="col-sm-4">';
+			if ($test_memory >= 0) {
+				echo '内存:', $test_memory, 'kb';
+			}
+			echo '</div>';
+
+			echo '</div>';
+			echo '</div>';
+
+			if (!$this->styler->shouldFadeDetails($test_info)) {
+				$accordion_collapse_class = 'card-collapse collapse';
+				if ($this->styler->collapse_in) {
+					$accordion_collapse_class .= ' in';
+				}
+				echo '<div id="', $accordion_collapse, '" class="', $accordion_collapse_class, '">';
+				echo '<div class="card-body">';
+
+				$this->_print_c($node);
+
+				echo '</div>';
+				echo '</div>';
+			}
+
+			echo '</div>';
+		} elseif ($node->nodeName == 'custom-test') {
+			$test_info = $node->getAttribute('info');
+			$test_time = $node->getAttribute('time');
+			$test_memory = $node->getAttribute('memory');
+
+			echo '<div class="card ', $this->styler->getTestInfoClass($test_info), ' mb-3">';
+			
+			$accordion_parent = "{$this->name}_details_accordion";
+			$accordion_collapse = "{$accordion_parent}_collapse_custom_test";
+			if (!$this->styler->shouldFadeDetails($test_info)) {
+				echo '<div class="card-header" data-toggle="collapse" data-parent="#', $accordion_parent, '" data-target="#', $accordion_collapse, '">';
+			} else {
+				echo '<div class="card-header">';
+			}
+			echo '<div class="row">';
+			echo '<div class="col-sm-2">';
+			echo '<h4 class="card-title">', '自定义测试:', '</h4>';
+			echo '</div>';
+				
+			echo '<div class="col-sm-4">';
+			echo htmlspecialchars($test_info);
+			echo '</div>';
+				
+			echo '<div class="col-sm-3">';
+			if ($test_time >= 0) {
+				echo '时间:', $test_time, 'ms';
+			}
+			echo '</div>';
+
+			echo '<div class="col-sm-3">';
+			if ($test_memory >= 0) {
+				echo '空间:', $test_memory, 'kb';
+			}
+			echo '</div>';
+
+			echo '</div>';
+			echo '</div>';
+
+			if (!$this->styler->shouldFadeDetails($test_info)) {
+				$accordion_collapse_class = 'card-collapse collapse';
+				if ($this->styler->collapse_in) {
+					$accordion_collapse_class .= ' in';
+				}
+				echo '<div id="', $accordion_collapse, '" class="', $accordion_collapse_class, '">';
+				echo '<div class="card-body">';
+
+				$this->_print_c($node);
+
+				echo '</div>';
+				echo '</div>';
+
+				echo '</div>';
+			}
+		} elseif ($node->nodeName == 'in') {
+			echo "<h4>标准输入:</h4><pre>\n";
+			$this->_print_c($node);
+			echo "\n</pre>";
+		} elseif ($node->nodeName == 'out') {
+			echo "<h4>标准输出:</h4><pre>\n";
+			$this->_print_c($node);
+			echo "\n</pre>";
+		} elseif ($node->nodeName == 'res') {
+			echo "<h4>检验结果:</h4><pre>\n";
+			$this->_print_c($node);
+			echo "\n</pre>";
+		} else {
+			echo '<', $node->nodeName;
+			foreach ($node->attributes as $attr) {
+				echo ' ', $attr->name, '="', htmlspecialchars($attr->value), '"';
+			}
+			echo '>';
+			$this->_print_c($node);
+			echo '</', $node->nodeName, '>';
+		}
+	}
+
+	public function __construct($details, $styler, $name) {
+		$this->name = $name;
+		$this->styler = $styler;
+		$this->details = $details;
+		$this->dom = new DOMDocument();
+		if (!$this->dom->loadXML($this->details)) {
+			throw new Exception("XML syntax error");
+		}
+		$this->details = '';
+	}
+	public function printHTML() {
+		$this->subtask_num = null;
+		$this->_print($this->dom->documentElement);
+	}
+}
+
+function echoJudgementDetails($raw_details, $styler, $name) {
+	try {
+		$printer = new JudgementDetailsPrinter($raw_details, $styler, $name);
+		$printer->printHTML();
+	} catch (Exception $e) {
+		echo 'Failed to show details';
+	}
+}
+
+class SubmissionDetailsStyler {
+	public $show_score = true;
+	public $show_small_tip = true;
+	public $collapse_in = false;
+	public $fade_all_details = false;
+	public function getTestInfoClass($info) {
+		if ($info == 'Accepted' || $info == 'Extra Test Passed') {
+			return 'card-uoj-accepted';
+		} elseif ($info == 'Time Limit Exceeded') {
+			return 'card-uoj-tle';
+		} elseif ($info == 'Acceptable Answer') {
+			return 'card-uoj-acceptable-answer';
+		} else {
+			return 'card-uoj-wrong';
+		}
+	}
+	public function shouldFadeDetails($info) {
+		return $this->fade_all_details || $info == 'Extra Test Passed';
+	}
+}
+class CustomTestSubmissionDetailsStyler {
+	public $show_score = true;
+	public $show_small_tip = false;
+	public $collapse_in = true;
+	public $fade_all_details = false;
+	public $ioi_contest_is_running = false;
+	public function getTestInfoClass($info) {
+		if ($info == 'Success') {
+			return 'card-uoj-accepted';
+		} elseif ($info == 'Time Limit Exceeded') {
+			return 'card-uoj-tle';
+		} elseif ($info == 'Acceptable Answer') {
+			return 'card-uoj-acceptable-answer';
+		} else {
+			return 'card-uoj-wrong';
+		}
+	}
+	public function shouldFadeDetails($info) {
+		return $this->fade_all_details;
+	}
+}
+class HackDetailsStyler {
+	public $show_score = false;
+	public $show_small_tip = false;
+	public $collapse_in = true;
+	public $fade_all_details = false;
+	public function getTestInfoClass($info) {
+		if ($info == 'Accepted' || $info == 'Extra Test Passed') {
+			return 'card-uoj-accepted';
+		} elseif ($info == 'Time Limit Exceeded') {
+			return 'card-uoj-tle';
+		} elseif ($info == 'Acceptable Answer') {
+			return 'card-uoj-acceptable-answer';
+		} else {
+			return 'card-uoj-wrong';
+		}
+	}
+	public function shouldFadeDetails($info) {
+		return $this->fade_all_details;
+	}
+}
+
+function echoSubmissionDetails($submission_details, $name) {
+	echoJudgementDetails($submission_details, new SubmissionDetailsStyler(), $name);
+}
+function echoCustomTestSubmissionDetails($submission_details, $name) {
+	echoJudgementDetails($submission_details, new CustomTestSubmissionDetailsStyler(), $name);
+}
+function echoHackDetails($hack_details, $name) {
+	echoJudgementDetails($hack_details, new HackDetailsStyler(), $name);
+}
+
+function echoHack($hack, $config, $user) {
+	$problem = queryProblemBrief($hack['problem_id']);
+	echo '<tr>';
+	if (!isset($config['id_hidden'])) {
+		echo '<td><a href="/hack/', $hack['id'], '">#', $hack['id'], '</a></td>';
+	}
+	if (!isset($config['submission_hidden'])) {
+		echo '<td><a href="/submission/', $hack['submission_id'], '">#', $hack['submission_id'], '</a></td>';
+	}
+	if (!isset($config['problem_hidden'])) {
+		if ($hack['contest_id']) {
+			echo '<td>', getContestProblemLink($problem, $hack['contest_id'], '!id_and_title'), '</td>';
+		} else {
+			echo '<td>', getProblemLink($problem, '!id_and_title'), '</td>';
+		}
+	}
+	if (!isset($config['hacker_hidden'])) {
+		echo '<td>', getUserLink($hack['hacker']), '</td>';
+	}
+	if (!isset($config['owner_hidden'])) {
+		echo '<td>', getUserLink($hack['owner']), '</td>';
+	}
+	if (!isset($config['result_hidden'])) {
+		if ($hack['judge_time'] == null) {
+			echo '<td><a href="/hack/', $hack['id'], '">Waiting</a></td>';
+		} elseif ($hack['success'] == null) {
+			echo '<td><a href="/hack/', $hack['id'], '">Judging</a></td>';
+		} elseif ($hack['success']) {
+			echo '<td><a href="/hack/', $hack['id'], '" class="uoj-status" data-success="1"><strong>Success!</strong></a></td>';
+		} else {
+			echo '<td><a href="/hack/', $hack['id'], '" class="uoj-status" data-success="0"><strong>Failed.</strong></a></td>';
+		}
+	} else {
+		echo '<td>Hidden</td>';
+	}
+	if (!isset($config['submit_time_hidden'])) {
+		echo '<td>', $hack['submit_time'], '</td>';
+	}
+	if (!isset($config['judge_time_hidden'])) {
+		echo '<td>', $hack['judge_time'], '</td>';
+	}
+	echo '</tr>';
+}
+function echoHackListOnlyOne($hack, $config, $user) {
+	echo '<div class="table-responsive shadow my-4">';
+	echo '<table class="table table-bordered table-text-center">';
+	echo '<thead>';
+	echo '<tr>';
+	if (!isset($config['id_hidden'])) {
+		echo '<th>ID</th>';
+	}
+	if (!isset($config['submission_id_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::submission id').'</th>';
+	}
+	if (!isset($config['problem_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::problem').'</th>';
+	}
+	if (!isset($config['hacker_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::hacker').'</th>';
+	}
+	if (!isset($config['owner_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::owner').'</th>';
+	}
+	if (!isset($config['result_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::result').'</th>';
+	}
+	if (!isset($config['submit_time_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::submit time').'</th>';
+	}
+	if (!isset($config['judge_time_hidden'])) {
+		echo '<th>'.UOJLocale::get('problems::judge time').'</th>';
+	}
+	echo '</tr>';
+	echo '</thead>';
+	echo '<tbody>';
+	echoHack($hack, $config, $user);
+	echo '</tbody>';
+	echo '</table>';
+	echo '</div>';
+}
+function echoHacksList($cond, $tail, $config, $user) {
+	$header_row = '<tr>';
+	$col_names = array();
+	
+	$col_names[] = 'id';
+	$col_names[] = 'success';
+	$col_names[] = 'judge_time';
+	
+	if (!isset($config['id_hidden'])) {
+		$header_row .= '<th>ID</th>';
+	}
+	if (!isset($config['submission_id_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::submission id').'</th>';
+		$col_names[] = 'submission_id';
+	}
+	if (!isset($config['problem_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::problem').'</th>';
+		$col_names[] = 'problem_id';
+	}
+	if (!isset($config['hacker_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::hacker').'</th>';
+		$col_names[] = 'hacker';
+	}
+	if (!isset($config['owner_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::owner').'</th>';
+		$col_names[] = 'owner';
+	}
+	if (!isset($config['result_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::result').'</th>';
+	}
+	if (!isset($config['submit_time_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::submit time').'</th>';
+		$col_names[] = 'submit_time';
+	}
+	if (!isset($config['judge_time_hidden'])) {
+		$header_row .= '<th>'.UOJLocale::get('problems::judge time').'</th>';
+	}
+	$header_row .= '</tr>';
+
+	if (!isSuperUser($user)) {
+		if ($user != null) {
+			$permission_cond = "is_hidden = false or (is_hidden = true and problem_id in (select problem_id from problems_permissions where username = '{$user['username']}'))";
+		} else {
+			$permission_cond = "is_hidden = false";
+		}
+		if ($cond !== '1') {
+			$cond = "($cond) and ($permission_cond)";
+		} else {
+			$cond = $permission_cond;
+		}
+	}
+
+	echoLongTable($col_names, 'hacks', $cond, $tail, $header_row,
+		function($hacks) use ($config, $user) {
+			echoHack($hacks, $config, $user);
+		}, null);
+}
+
+function echoDatum($datum) {
+	$pid = $datum['pid'];
+	$title = queryProblemBrief($pid)['title'];
+
+	echo '<tr>';
+	echo '<td>' . $datum['id'] . '</td>';
+	echo '<td>', getProblemLink(queryProblemBrief($pid), '!id_and_title'), '</td>';
+
+	if ($datum['status'] == 'Done')
+		echo '<td><a href="/datum/' . $datum['id'] . '" class="uoj-status" data-success="1"><strong>Done</strong></a></td>';
+	else if ($datum['status'] == 'Failed')
+		echo '<td><a href="/datum/' . $datum['id'] . '" class="uoj-status" data-success="0"><strong>Failed</strong></a></td>';
+	else	echo '<td><a href="/datum/' . $datum['id'] . '">' . $datum['status'] . '</a></td>';
+	
+	echo '<td>' . $datum['request_time'] . '</td>';
+	echo '<td>' . $datum['finish_time'] . '</td>';
+	echo '<td>' . $datum['timelimit'] . ' s</td>';
+	echo '<td>' . $datum['memlimit'] . ' MB</td>';
+	echo '</tr>';
+}
+
+function echoDatumlistOnlyOne($datum) {
+	$header_row = '<tr><th>ID</th><th>题目</th><th>状态</th><th>请求时间</th><th>完成时间</th><th>时间限制</th><th>空间限制</th></tr>';
+	echo '<div class="table-responsive shadow my-4">';
+	echo '<table class="table table-bordered table-text-center">';
+	echo '<thead>';
+	echo $header_row;
+	echo '</thead>';
+	echo '<tbody>';
+	echoDatum($datum);
+	echo '</tbody>';
+	echo '</table></div>';
+}
+
+function echoDatumList() {
+	$col_names = array('id', 'pid', 'request_time', 'status', 'finish_time', 'timelimit', 'memlimit');
+	$header_row = '<tr><th>ID</th><th>题目</th><th>状态</th><th>请求时间</th><th>完成时间</th><th>时间限制</th><th>空间限制</th></tr>';
+	echoLongTable($col_names, 'datum_requests', '1', '', $header_row,
+		function($datum) { echoDatum($datum); }, null);
+}
+
+function getBlogPreview($str) {
+	return '';
+}
+
+function echoBlogCard($blog) {
+	if ($blog == null)
+		return;
+	$bloghref = "/blogs/" . $blog['id'];
+	$blogtitle = $blog['title'];
+	$poster = queryUser($blog['poster']);
+	$blogpreview = getBlogPreview($blog['content']);
+	$userlink = getUserLink($blog['poster']);
+	$posttime = $blog['post_time'];
+	$usermotto = $poster['motto'];
+	$zanblock = getClickZanBlock('B', $blog['id'], $blog['zan']);
+
+	return <<<EOF
+	<div class="col-12 mb-4">
+		<div class="card d-flex p-3 shadow mx-4">
+			<div class="align-items-start">
+				<a href="$bloghref"><h5>$blogtitle</h5></a>
+				<p>$blogpreview</p>
+			</div>
+			<div class="d-flex align-items-end justify-content-between">
+				<div>
+					<span class="mb-2">$userlink</span>
+					<span class="text-muted">$posttime</span>
+					<span class="ml-4">$usermotto</span>
+				</div>
+				$zanblock
+			</div>
+		</div>
+	</div>
+EOF;
+}
+
+function echoRecommendProblem($pid, $link, $name, $descr) {
+	return <<<EOF
+	<div class="col-md-3 mb-4">
+		<div class="card h-100 p-3 text-center">
+			<a class="mb-2" href="$link"><h4>$pid</h4></a>
+			<h4 class="mb-2">$name</h4>
+			<span class="text-bottom">$descr</span>
+		</div>
+	</div>
+EOF;
+}
+
+function echoBlog($blog, $config = array()) {
+	$default_config = array(
+		'blog' => $blog,
+		'show_title_only' => false,
+		'show_content_only' => false,
+		'is_preview' => false,
+		'outside_blog_domain' => false,
+	);
+	foreach ($default_config as $key => $val) {
+		if (!isset($config[$key])) {
+			$config[$key] = $val;
+		}
+	}
+	uojIncludeView('blog-preview', $config);
+}
+function echoBlogTag($tag) {
+	echo '<a class="uoj-blog-tag"><span class="badge badge-pill badge-secondary">', HTML::escape($tag), '</span></a>';
+}
+
+function echoUOJPageHeader($page_title, $extra_config = array()) {
+	global $REQUIRE_LIB;
+	$config = UOJContext::pageConfig();
+	$config['REQUIRE_LIB'] = $REQUIRE_LIB;
+	$config['PageTitle'] = $page_title;
+	$config = array_merge($config, $extra_config);
+	uojIncludeView('page-header', $config);
+}
+function echoUOJPageFooter($config = array()) {
+	uojIncludeView('page-footer', $config);
+}
+
+function getAvatar($user) {
+	return '<img class="media-object img-rounded" height="50" width="50" src="' . HTML::avatar_addr($user, 50) . '">';
+}
+
+function echoRanklist($config = array()) {
+	$header_row = '';
+	$header_row .= '<tr>';
+	$header_row .= '<th style="width: 5em;">#</th>';
+	$header_row .= '<th style="width: 14em";>'.'头像'.'</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('rating').'</th>';
+	$header_row .= '</tr>';
+	
+	$users = array();
+	$print_row = function($user, $now_cnt) use (&$users) {
+		if (!$users) {
+			$rank = DB::selectCount("select count(*) from user_info where rating > {$user['rating']}") + 1;
+		} elseif ($user['rating'] == $users[count($users) - 1]['rating']) {
+			$rank = $users[count($users) - 1]['rank'];
+		} else {
+			$rank = $now_cnt;
+		}
+		
+		$user['rank'] = $rank;
+		
+		echo '<tr>';
+		echo '<td>' . $user['rank'] . '</td>';
+		echo '<td>' . getAvatar($user) . '</td>';
+		echo '<td>' . getUserLink($user['username']) . '</td>';
+		echo '<td>' . HTML::escape($user['motto']) . '</td>';
+		echo '<td>' . $user['rating'] . '</td>';
+		echo '</tr>';
+		
+		$users[] = $user;
+	};
+	$col_names = array('username', 'email', 'qq', 'rating', 'motto');
+	$tail = 'order by rating desc, username asc';
+	
+	if (isset($config['top10'])) {
+		$tail .= ' limit 10';
+	}
+	
+	$config['get_row_index'] = '';
+	echoLongTable($col_names, 'user_info', '1', $tail, $header_row, $print_row, $config);
+}
+
+

+ 186 - 0
app/libs/uoj-judger-lib.php

@@ -0,0 +1,186 @@
+<?php
+	global $uojSupportedLanguages, $uojMainJudgerWorkPath;
+	$uojSupportedLanguages = array('C', 'C++14', 'C++17', 'C++17ubsan', 'Python3');
+	$uojMainJudgerWorkPath = "/opt/uoj/judger/uoj_judger";
+	
+	function authenticateJudger() {
+		if (!is_string($_POST['judger_name']) || !is_string($_POST['password'])) {
+			return false;
+		}
+		$esc_judger_name = DB::escape($_POST['judger_name']);
+		$judger = DB::selectFirst("select password from judger_info where judger_name = '$esc_judger_name'");
+		if ($judger == null) {
+			return false;
+		}
+		return $judger['password'] == $_POST['password'];
+	}
+	
+	function judgerCodeStr($code) {
+		switch ($code) {
+			case 0:
+				return "Accepted";
+			case 1:
+				return "Wrong Answer";
+			case 2:
+				return "Runtime Error";
+			case 3:
+				return "Memory Limit Exceeded";
+			case 4:
+				return "Time Limit Exceeded";
+			case 5:
+				return "Output Limit Exceeded";
+			case 6:
+				return "Dangerous Syscalls";
+			case 7:
+				return "Judgement Failed";
+			default:
+				return "No Comment";
+		}
+	}
+	
+	class StrictFileReader {
+		private $f;
+		private $buf = '', $off = 0;
+
+		public function __construct($file_name) {
+			$this->f = fopen($file_name, 'r');
+		}
+
+		public function failed() {
+			return $this->f === false;
+		}
+
+		public function readChar() {
+			if (isset($this->buf[$this->off])) {
+				return $this->buf[$this->off++];
+			}
+			return fgetc($this->f);
+		}
+		public function unreadChar($c) {
+			$this->buf .= $c;
+			if ($this->off > 1000) {
+				$this->buf = substr($this->buf, $this->off);
+				$this->off = 0;
+			}
+		}
+
+		public function readString() {
+			$str = '';
+			while (true) {
+				$c = $this->readChar();
+				if ($c === false) {
+					break;
+				} elseif ($c === " " || $c === "\n" || $c === "\r") {
+					$this->unreadChar($c);
+					break;
+				} else {
+					$str .= $c;
+				}
+			}
+			return $str;
+		}
+		public function ignoreWhite() {
+			while (true) {
+				$c = $this->readChar();
+				if ($c === false) {
+					break;
+				} elseif ($c === " " || $c === "\n" || $c === "\r") {
+					continue;
+				} else {
+					$this->unreadChar($c);
+					break;
+				}
+			}
+		}
+
+		public function eof() {
+			return feof($this->f);
+		}
+
+		public function close() {
+			fclose($this->f);
+		}
+	}
+
+	function getUOJConf($file_name) {
+		$reader = new StrictFileReader($file_name);
+		if ($reader->failed()) {
+			return -1;
+		}
+
+		$conf = array();
+		while (!$reader->eof()) {
+			$reader->ignoreWhite();
+			$key = $reader->readString();
+			if ($key === '') {
+				break;
+			}
+			$reader->ignoreWhite();
+			$val = $reader->readString();
+			if ($val === '') {
+				break;
+			}
+
+			if (isset($conf[$key])) {
+				return -2;
+			}
+			$conf[$key] = $val;
+		}
+		$reader->close();
+		return $conf;
+	}
+	function putUOJConf($file_name, $conf) {
+		$f = fopen($file_name, 'w');
+		foreach ($conf as $key => $val) {
+			fwrite($f, "$key $val\n");
+		}
+		fclose($f);
+	}
+	
+	function getUOJConfVal($conf, $key, $default_val) {
+		if (isset($conf[$key])) {
+			return $conf[$key];
+		} else {
+			return $default_val;
+		}
+	}
+	
+	function getUOJProblemInputFileName($problem_conf, $num) {
+		return getUOJConfVal($problem_conf, 'input_pre', 'input') . $num . '.' . getUOJConfVal($problem_conf, 'input_suf', 'txt');
+	}
+	function getUOJProblemOutputFileName($problem_conf, $num) {
+		return getUOJConfVal($problem_conf, 'output_pre', 'output') . $num . '.' . getUOJConfVal($problem_conf, 'output_suf', 'txt');
+	}
+	function getUOJProblemExtraInputFileName($problem_conf, $num) {
+		return 'ex_' . getUOJConfVal($problem_conf, 'input_pre', 'input') . $num . '.' . getUOJConfVal($problem_conf, 'input_suf', 'txt');
+	}
+	function getUOJProblemExtraOutputFileName($problem_conf, $num) {
+		return 'ex_' . getUOJConfVal($problem_conf, 'output_pre', 'output') . $num . '.' . getUOJConfVal($problem_conf, 'output_suf', 'txt');
+	}
+	
+	function rejudgeProblem($problem) {
+		DB::query("update submissions set judge_time = NULL , result = '' , score = NULL , status = 'Waiting Rejudge' where problem_id = ${problem['id']}");
+	}
+	function rejudgeProblemAC($problem) {
+		DB::query("update submissions set judge_time = NULL , result = '' , score = NULL , status = 'Waiting Rejudge' where problem_id = ${problem['id']} and score = 100");
+	}
+	function rejudgeProblemGe97($problem) {
+		DB::query("update submissions set judge_time = NULL , result = '' , score = NULL , status = 'Waiting Rejudge' where problem_id = ${problem['id']} and score >= 97");
+	}
+	function rejudgeSubmission($submission) {
+		DB::query("update submissions set judge_time = NULL , result = '' , score = NULL , status = 'Waiting Rejudge' where id = ${submission['id']}");
+	}
+	function updateBestACSubmissions($username, $problem_id) {
+		$best = DB::selectFirst("select id, used_time, used_memory, tot_size from submissions where submitter = '$username' and problem_id = $problem_id and score = 100 order by used_time, used_memory, tot_size asc limit 1");
+		$shortest = DB::selectFirst("select id, used_time, used_memory, tot_size from submissions where submitter = '$username' and problem_id = $problem_id and score = 100 order by tot_size, used_time, used_memory asc limit 1");
+		DB::delete("delete from best_ac_submissions where submitter = '$username' and problem_id = $problem_id");
+		if ($best) {
+			DB::insert("insert into best_ac_submissions (problem_id, submitter, submission_id, used_time, used_memory, tot_size, shortest_id, shortest_used_time, shortest_used_memory, shortest_tot_size) values ($problem_id, '$username', ${best['id']}, ${best['used_time']}, ${best['used_memory']}, ${best['tot_size']}, ${shortest['id']}, ${shortest['used_time']}, ${shortest['used_memory']}, ${shortest['tot_size']})");
+		}
+
+		$cnt = DB::selectCount("select count(*) from best_ac_submissions where submitter='$username'");
+		DB::update("update user_info set ac_num = $cnt where username='$username'");
+		
+		DB::update("update problems set ac_num = (select count(*) from submissions where problem_id = problems.id and score = 100), submit_num = (select count(*) from submissions where problem_id = problems.id) where id = $problem_id");
+	}
+?>

+ 33 - 0
app/libs/uoj-lib.php

@@ -0,0 +1,33 @@
+<?php
+	error_reporting(E_ALL ^ E_NOTICE);
+	
+	spl_autoload_register(function($class_name) {
+		require_once $_SERVER['DOCUMENT_ROOT'] . '/app/models/' . $class_name . '.php';
+	});
+	
+	function requireLib($name) { // html lib
+		global $REQUIRE_LIB;
+		$REQUIRE_LIB[$name] = '';
+	}
+	function requirePHPLib($name) { // uoj php lib
+		require $_SERVER['DOCUMENT_ROOT'].'/app/libs/uoj-'.$name.'-lib.php';
+	}
+	
+	requirePHPLib('validate');
+	requirePHPLib('query');
+	requirePHPLib('rand');
+	requirePHPLib('utility');
+	requirePHPLib('security');
+	requirePHPLib('contest');
+	requirePHPLib('html');
+	
+	Session::init();
+	UOJTime::init();
+	DB::init();
+	Auth::init();
+	
+	if (isset($_GET['locale'])) {
+		UOJLocale::setLocale($_GET['locale']);
+	}
+	UOJLocale::requireModule('basic');
+?>

+ 354 - 0
app/libs/uoj-query-lib.php

@@ -0,0 +1,354 @@
+<?php
+
+function hasProblemPermissionGroup($group, $problem) {
+	if ($group == null)
+		return false;
+	return in_array($problem['id'], explode(',', $group['privs']));
+}
+
+function hasProblemViewPermission($user, $problem) {
+	if ($user == null)
+		return false;
+	if (isSuperUser($user))
+		return true;
+	if (DB::selectFirst("select * from problems_permissions where username = '{$user['username']}' and problem_id = {$problem['id']}") != null)
+		return true;
+	$groups = explode(',', $user['extgroups']);
+	foreach ($groups as $key => $grp)
+		if (hasProblemPermissionGroup(queryUser($grp), $problem))
+			return true;
+	return false;
+}
+
+function hasProblemCtrlPermission($user, $problem) {
+	if (!hasProblemViewPermission($user, $problem))
+		return false;
+	if (isSuperUser($user))
+		return true;
+	$grp = $problem['ctrl'];
+	if ($grp == '') return false;
+	$grp = queryUser($grp);
+	if (!$grp) return false;
+	if (in_array($user['username'], explode(',', $grp['ctrls'])))
+		return true;
+	return false;
+}
+
+function giveGroupPermission($user, $group) {
+	if ($user == null || $group == null) return;
+	$q = explode(',', $group['ctrls']);
+	if (in_array($user['username'], $q)) return;
+	$q[] = $user['username'];
+	$s = join(',', $q);
+	$groupname = $group['username'];
+	DB::update("update user_info set ctrls = '$s' where username = '$groupname'");
+}
+
+function removeGroupPermission($user, $group) {
+	if ($user == null || $group == null) return;
+	$q = explode(',', $group['ctrls']);
+	$p = array();
+	foreach ($q as $i => $k) if ($k != $user['username']) $p[] = $k;
+	$s = join(',', $p);
+	$groupname = $group['username'];
+	DB::update("update user_info set ctrls = '$s' where username = '$groupname'");
+}
+
+function attachProblemToGroup($problem, $group) {
+	if ($problem == null || $group == null)
+		return;
+	if (in_array($problem['id'], explode(',', $group['privs'])))
+		return;
+	if ($group['privs'] == '') $privs = $problem['id'];
+	else $privs = $group['privs'] . ',' . $problem['id'];
+	$name = $group['username'];
+	DB::update("update user_info set privs = '$privs' where username = '$name'");
+}
+
+function attachUserToGroup($user, $group) {
+	if ($user == null || $group == null)
+		return;
+	if (in_array($group['username'], explode(',', $user['extgroups'])))
+		return;
+	if ($user['extgroups'] == '') $extgrps = $group['username'];
+	else $extgrps = $user['extgroups'] . ',' . $group['username'];
+	$name = $user['username'];
+	DB::update("update user_info set extgroups = '$extgrps' where username = '$name'");
+}
+
+function deattachUserFromGroup($user, $group) {
+	if ($user == null || $group == null) return;
+	removeGroupPermission($user, $group);
+	$q = explode(',', $user['extgroups']);
+	if (!in_array($group['username'], $q)) return;
+	$p = array();
+	foreach ($q as $i => $k) if ($k != $group['username']) $p[] = $k;
+	$s = join(',', $p);
+	$name = $user['username'];
+	DB::update("update user_info set extgroups = '$s' where username = '$name'");
+}
+
+function queryUserExtgroups($user) {
+	if ($user == null) return;
+	return explode(',', $user['extgroups']);
+}
+
+function queryGroupControllers($grp) {
+	if ($grp == null) return;
+	return explode(',', $grp['ctrls']);
+}
+
+function queryGroupMembers($grp) {
+	$grpname = $grp['username'];
+	return DB::selectAll("select username from user_info where find_in_set('$grpname', extgroups)");
+}
+
+function queryGroupProblems($grp) {
+	$p = $grp['privs'];
+	if ($p == '') return array ();
+	return explode(',', $grp['privs']);
+}
+
+function queryProblemSize($pid) {
+	return filesize("/var/uoj_data/$pid.zip");
+}
+
+function queryGroupTotalSize($grp) {
+	$size = 0;
+	foreach (queryGroupProblems($grp) as $i => $pid)
+		if (queryProblemBrief($pid)['ctrl'] == $grp['username'])
+			$size += queryProblemSize($pid);
+	return $size;
+}
+
+function queryGroupCtrlProblems($grp) {
+	$q = array();
+	foreach (queryGroupProblems($grp) as $i => $pid)
+		if (queryProblemBrief($pid)['ctrl'] == $grp['username']) $q[] = $pid;
+	return $q;
+}
+
+function queryGroupQuota($grp) {
+	return $grp['ac_num'] * 1048576;
+}
+
+function queryGroupProblemQuota($grp) {
+	return 20;
+}
+
+function queryUserCtrlgroups($user) {
+	if ($user == null) return;
+	if (isSuperUser($user)) {
+		$q = array();
+		foreach (DB::selectAll("select username from user_info where username like 'group_%'") as $i => $k)
+			$q[] = $k['username'];
+		return $q;
+	}
+	$res = array();
+	foreach (queryUserExtgroups($user) as $i => $grp) {
+		if (in_array($user['username'], queryGroupControllers(queryUser($grp))))
+			$res[] = $grp;
+	}
+	return $res;
+}
+
+function hasGroupCtrlPermission($user, $grp) {
+	if ($user == null) return false;
+	if (isSuperUser($user)) return true;
+	if (in_array($user['username'], explode(',', $grp['ctrls'])))
+		return true;
+	else return false;
+}
+
+function hasViewPermission($str,$user,$problem,$submission) {
+	if ($str=='ALL') {
+		return true;
+	}
+	if ($str=='ALL_AFTER_AC') {
+		return hasAC($user,$problem);
+	}
+	if ($str=='SELF') {
+		return $submission['submitter']==$user['username'];
+	}
+	return false;
+}
+
+function hasContestPermission($user, $contest) {
+	if ($user == null) {
+		return false;
+	}
+	if (isSuperUser($user)) {
+		return true;
+	}
+	return DB::selectFirst("select * from contests_permissions where username = '{$user['username']}' and contest_id = {$contest['id']}") != null;
+}
+
+function hasRegistered($user, $contest) {
+	return DB::selectFirst("select * from contests_registrants where username = '${user['username']}' and contest_id = ${contest['id']}") != null;
+}
+function hasAC($user, $problem) {
+	return DB::selectFirst("select * from best_ac_submissions where submitter = '${user['username']}' and problem_id = ${problem['id']}") != null;
+}
+
+function queryUser($username) {
+	if (!validateUsername($username)) {
+		return null;
+	}
+	return DB::selectFirst("select * from user_info where username='$username'", MYSQLI_ASSOC);
+}
+
+function queryProblemContent($id) {
+	return DB::selectFirst("select * from problems_contents where id = $id", MYSQLI_ASSOC);
+}
+function queryProblemBrief($id) {
+	return DB::selectFirst("select * from problems where id = $id", MYSQLI_ASSOC);
+}
+
+function queryRemoteProblemBrief($pid) {
+	return DB::selectFirst("select pid, title from rp where pid = '$pid'", MYSQLI_ASSOC);
+}
+
+function queryRemoteProblemFull($pid) {
+	return DB::selectFirst("select pid, title, statement from rp where pid = '$pid'", MYSQLI_ASSOC);
+}
+
+function queryProblemTags($id) {
+	$tags = array();
+	$result = DB::query("select tag from problems_tags where problem_id = $id order by id");
+	while ($row = DB::fetch($result, MYSQLI_NUM)) {
+		$tags[] = $row[0];
+	}
+	return $tags;
+}
+function queryContestProblemRank($contest, $problem) {
+	if (!DB::selectFirst("select * from contests_problems where contest_id = {$contest['id']} and problem_id = {$problem['id']}")) {
+		return null;
+	}
+	return DB::selectCount("select count(*) from contests_problems where contest_id = {$contest['id']} and problem_id <= {$problem['id']}");
+}
+
+function queryDatumStream($id) {
+	return DB::selectFirst("select * from datum_streams where id = $id", MYSQLI_ASSOC);
+}
+
+function querySubmission($id) {
+	return DB::selectFirst("select * from submissions where id = $id", MYSQLI_ASSOC);
+}
+function queryHack($id) {
+	return DB::selectFirst("select * from hacks where id = $id", MYSQLI_ASSOC);
+}
+function queryContest($id) {
+	return DB::selectFirst("select * from contests where id = $id", MYSQLI_ASSOC);
+}
+function queryContestProblem($id) {
+	return DB::selectFirst("select * from contest_problems where contest_id = $id", MYSQLI_ASSOC);
+}
+
+function queryDatumBrief($id) {
+	return DB::selectFirst("select id, pid, status from datum_requests where id = $id", MYSQLI_ASSOC);
+}
+
+function queryDatum($id) {
+	return DB::selectFirst("select * from datum_requests where id = $id", MYSQLI_ASSOC);
+}
+
+function queryZanVal($id, $type, $user) {
+	if ($user == null) {
+		return 0;
+	}
+	$esc_type = DB::escape($type);
+	$row = DB::selectFirst("select val from click_zans where username='{$user['username']}' and type='$esc_type' and target_id='$id'");
+	if ($row == null) {
+		return 0;
+	}
+	return $row['val'];
+}
+
+function queryBlog($id) {
+	return DB::selectFirst("select * from blogs where id='$id'", MYSQLI_ASSOC);
+}
+function queryBlogTags($id) {
+	$tags = array();
+	$result = DB::select("select tag from blogs_tags where blog_id = $id order by id");
+	while ($row = DB::fetch($result, MYSQLI_NUM)) {
+		$tags[] = $row[0];
+	}
+	return $tags;
+}
+function queryBlogComment($id) {
+	return DB::selectFirst("select * from blogs_comments where id='$id'", MYSQLI_ASSOC);
+}
+
+function isProblemVisibleToUser($problem, $user) {
+	return !$problem['is_hidden'] || hasProblemViewPermission($user, $problem);
+}
+function isContestProblemVisibleToUser($problem, $contest, $user) {
+	if (isProblemVisibleToUser($problem, $user)) {
+		return true;
+	}
+	if ($contest['cur_progress'] >= CONTEST_PENDING_FINAL_TEST) {
+		return true;
+	}
+	if ($contest['cur_progress'] == CONTEST_NOT_STARTED) {
+		return false;
+	}
+	return hasRegistered($user, $contest);
+}
+
+function isSubmissionVisibleToUser($submission, $problem, $user) {
+	if (isSuperUser($user)) {
+		return true;
+	} elseif (!$submission['is_hidden']) {
+		return true;
+	} else {
+		return hasProblemViewPermission($user, $problem);
+	}
+}
+function isHackVisibleToUser($hack, $problem, $user) {
+	if (isSuperUser($user)) {
+		return true;
+	} elseif (!$hack['is_hidden']) {
+		return true;
+	} else {
+		return hasProblemViewPermission($user, $problem);
+	}
+}
+
+function isSubmissionFullVisibleToUser($submission, $contest, $problem, $user) {
+	if (isSuperUser($user)) {
+		return true;
+	} elseif (!$contest) {
+		return true;
+	} elseif ($contest['cur_progress'] > CONTEST_IN_PROGRESS) {
+		return true;
+	} elseif ($submission['submitter'] == $user['username']) {
+		return true;
+	} else {
+		return hasProblemCtrlPermission($user, $problem);
+	}
+}
+function isHackFullVisibleToUser($hack, $contest, $problem, $user) {
+	if (isSuperUser($user)) {
+		return true;
+	} elseif (!$contest) {
+		return true;
+	} elseif ($contest['cur_progress'] > CONTEST_IN_PROGRESS) {
+		return true;
+	} elseif ($hack['hacker'] == $user['username']) {
+		return true;
+	} else {
+		return hasProblemCtrlPermission($user, $problem);
+	}
+}
+
+function deleteBlog($id) {
+	if (!validateUInt($id)) {
+		return;
+	}
+	DB::delete("delete from click_zans where type = 'B' and target_id = $id");
+	DB::delete("delete from click_zans where type = 'BC' and target_id in (select id from blogs_comments where blog_id = $id)");
+	DB::delete("delete from blogs where id = $id");
+	DB::delete("delete from blogs_comments where blog_id = $id");
+	DB::delete("delete from important_blogs where blog_id = $id");
+	DB::delete("delete from blogs_tags where blog_id = $id");
+}

+ 37 - 0
app/libs/uoj-rand-lib.php

@@ -0,0 +1,37 @@
+<?php
+
+function uojRand($l, $r) {
+	return mt_rand($l, $r);
+}
+
+function uojRandString($len, $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
+	$n_chars = strlen($charset);
+	$str = '';
+	for ($i = 0; $i < $len; $i++) {
+		$str .= $charset[uojRand(0, $n_chars - 1)];
+	}
+	return $str;
+}
+
+function uojRandAvaiableFileName($dir) {
+	do {
+		$fileName = $dir . uojRandString(20);
+	} while (file_exists(UOJContext::storagePath().$fileName));
+	return $fileName;
+}
+
+function uojRandAvaiableTmpFileName() {
+	return uojRandAvaiableFileName('/tmp/');
+}
+
+function uojRandAvaiableSubmissionFileName() {
+	$num = uojRand(1, 10000);
+	if (!file_exists(UOJContext::storagePath()."/submission/$num")) {
+		system("mkdir ".UOJContext::storagePath()."/submission/$num");
+	}
+	return uojRandAvaiableFileName("/submission/$num/");
+}
+
+function uojRandAvaiablePasteFileName() {
+	return uojRandAvaiableFileName('/paste/');
+}

+ 33 - 0
app/libs/uoj-security-lib.php

@@ -0,0 +1,33 @@
+<?php
+
+function getPasswordToStore($password, $username) {
+	return md5($username . $password);
+}
+function checkPassword($user, $password) {
+	return $user['password'] == md5($user['username'] . $password);
+}
+function getPasswordClientSalt() {
+	return UOJConfig::$data['security']['user']['client_salt'];
+}
+
+function crsf_token() {
+	if (!isset($_SESSION['_token'])) {
+		$_SESSION['_token'] = uojRandString(60);
+	}
+	return $_SESSION['_token'];
+}
+function crsf_check() {
+	if (isset($_POST['_token'])) {
+		$_token = $_POST['_token'];
+	} elseif (isset($_GET['_token'])) {
+		$_token = $_GET['_token'];
+	} else {
+		return false;
+	}
+	return $_token === $_SESSION['_token'];
+}
+function crsf_defend() {
+	if (!crsf_check()) {
+		becomeMsgPage('This page has expired.');
+	}
+}

+ 187 - 0
app/libs/uoj-utility-lib.php

@@ -0,0 +1,187 @@
+<?php
+
+function mergeConfig(&$config, $default_config) {
+	foreach ($default_config as $key => $val) {
+		if (!isset($config[$key])) {
+			$config[$key] = $val;
+		} elseif (is_array($config[$key])) {
+			mergeConfig($config[$key], $val);
+		}
+	}
+}
+
+function printSize($size) {
+	if ($size <= 1024) return "$size Bytes";
+	if ($size <= 1048576 / 2) return sprintf("%.3lf KB", $size / 1024);
+	return sprintf("%.3lf MB", $size / 1048576);
+}
+
+function strStartWith($str, $pre) {
+	return substr($str, 0, strlen($pre)) === $pre;
+}
+
+function strEndWith($str, $suf) {
+	return substr($str, -strlen($suf)) === $suf;
+}
+
+function strOmit($str, $len) {
+	if (strlen($str) <= $len + 3) {
+		return $str;
+	} else {
+		return substr($str, 0, $len) . '...';
+	}
+}
+
+function uojTextEncode($str, $config = array()) {
+	mergeConfig($config, [
+		'allow_CR' => false,
+		'html_escape' => false
+	]);
+	
+	$allow = array();
+	for ($c = 32; $c <= 126; $c++) {
+		$allow[chr($c)] = true;
+	}
+	$allow["\n"] = true;
+	$allow[" "] = true;
+	$allow["\t"] = true;
+	
+	if ($config['allow_CR']) {
+		$allow["\r"] = true;
+	}
+	
+	$len = strlen($str);
+	$ok = true;
+	for ($i = 0; $i < $len; $i++) {
+		$c = $str[$i];
+		if (!isset($allow[$c])) {
+			$ok = false;
+		}
+	}
+	if ($ok && mb_check_encoding($str, 'utf-8')) {
+		if (!$config['html_escape']) {
+			return $str;
+		} else {
+			return HTML::escape($str);
+		}
+	} else {
+		$len = strlen($str);
+		$res = '';
+		$i = 0;
+		while ($i < $len) {
+			$c = $str[$i];
+			if (ord($c) < 128) {
+				if (isset($allow[$c])) {
+					if ($config['html_escape']) {
+						if ($c == '&') {
+							$res .= '&amp;';
+						} elseif ($c == '"') {
+							$res .= '&quot;';
+						} elseif ($c == '<') {
+							$res .= '&lt;';
+						} elseif ($c == '>') {
+							$res .= '&gt;';
+						} else {
+							$res .= $c;
+						}
+					} else {
+						$res .= $c;
+					}
+				} else {
+					$res .= '<b>\x' . bin2hex($c) . '</b>';
+				}
+				$i++;
+			} else {
+				$ok = false;
+				$cur = $c;
+				for ($j = $i + 1; $j < $i + 4 && $j < $len; $j++) {
+					$cur .= $str[$j];
+					if (mb_check_encoding($cur, 'utf-8')) {
+						$ok = true;
+						break;
+					}
+				}
+				if ($ok) {
+					$res .= $cur;
+					$i = $j + 1;
+				} else {
+					$res .= '<b>\x' . bin2hex($c) . '</b>';
+					$i++;
+				}
+			}
+		}
+		return $res;
+	}
+}
+
+function base64url_encode($data) {
+	return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
+}
+function base64url_decode($data) {
+	return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
+}
+
+function blog_name_encode($name) {
+	$name = str_replace('-', '_', $name);
+	if (!strStartWith($name, '_') && !strEndWith($name, '_')) {
+		$name = str_replace('_', '-', $name);
+	}
+	$name = strtolower($name);
+	return $name;
+}
+function blog_name_decode($name) {
+	$name = str_replace('-', '_', $name);
+	$name = strtolower($name);
+	return $name;
+}
+
+function isSuperUser($user) {
+	return $user != null && $user['usergroup'] == 'S';
+}
+function getProblemExtraConfig($problem) {
+	$extra_config = json_decode($problem['extra_config'], true);
+	
+	$default_extra_config = array(
+		'view_content_type' => 'ALL',
+		'view_all_details_type' => 'ALL',
+		'view_details_type' => 'ALL'
+	);
+	
+	mergeConfig($extra_config, $default_extra_config);
+	
+	return $extra_config;
+}
+function getProblemSubmissionRequirement($problem) {
+	return json_decode($problem['submission_requirement'], true);
+}
+function getProblemCustomTestRequirement($problem) {
+	$extra_config = json_decode($problem['extra_config'], true);
+	if (isset($extra_config['custom_test_requirement'])) {
+		return $extra_config['custom_test_requirement'];
+	} else {
+		$answer = array(
+			'name' => 'answer',
+			'type' => 'source code',
+			'file_name' => 'answer.code'
+		);
+		foreach (getProblemSubmissionRequirement($problem) as $req) {
+			if ($req['name'] == 'answer' && $req['type'] == 'source code' && isset($req['languages'])) {
+				$answer['languages'] = $req['languages'];
+			}
+		}
+		return array(
+			$answer,
+			array(
+				'name' => 'input',
+				'type' => 'text',
+				'file_name' => 'input.txt'
+			)
+		);
+	}
+}
+
+function sendSystemMsg($username, $title, $content) {
+	$content = DB::escape($content);
+	$title = DB::escape($title);
+	DB::insert("insert into user_system_msg (receiver, title, content, send_time) values ('$username', '$title', '$content', now())");
+}

+ 49 - 0
app/libs/uoj-validate-lib.php

@@ -0,0 +1,49 @@
+<?php
+
+function validateUsername($username) {
+	return is_string($username) && preg_match('/^[a-zA-Z0-9_]{1,20}$/', $username);
+}
+
+function validatePassword($password) {
+	return is_string($password) && preg_match('/^[a-z0-9]{32}$/', $password);
+}
+
+function validateEmail($email) {
+	return is_string($email) && strlen($email) <= 50 && preg_match('/^(.+)@(.+)$/', $email);
+}
+
+function validateQQ($QQ) {
+	return is_string($QQ) && strlen($QQ) <= 15 && preg_match('/^[0-9]{5,15}$/', $QQ);
+}
+
+function validateMotto($motto) {
+	return is_string($motto) && ($len = mb_strlen($motto, 'UTF-8')) !== false && $len <= 50;
+}
+
+function validateUInt($x) { // [0, 1000000000)
+	if (!is_string($x)) {
+		return false;
+	}
+	if ($x === '0') {
+		return true;
+	}
+	return preg_match('/^[1-9][0-9]{0,8}$/', $x);
+}
+
+function validateInt($x) {
+	if (!is_string($x)) {
+		return false;
+	}
+	if ($x[0] == '-') {
+		$x = substr($x, 1);
+	}
+	return validateUInt($x);
+}
+
+function validateUploadedFile($name) {
+	return isset($_FILES[$name]) && is_uploaded_file($_FILES[$name]['tmp_name']);
+}
+
+function validateIP($ip) {
+	return filter_var($ip, FILTER_VALIDATE_IP) !== false;
+}

+ 69 - 0
app/locale/basic/en.php

@@ -0,0 +1,69 @@
+<?php
+return [
+	'_common_name' => 'English',
+	'login' => 'Login',
+	'register' => 'Register',
+	'logout' => 'Logout',
+	'my profile' => 'My Profile',
+	'private message' => 'Private Message',
+	'system message' => 'System Message',
+	'system manage' => 'System Manage',
+	'contests' => 'Contests',
+	'problems' => 'Problems',
+	'submissions' => 'Submissions',
+	'hacks' => 'Hack!',
+	'blogs' => 'Blogs',
+	'announcements' => 'Announcements',
+	'all the announcements' => 'All the Announcements……',
+	'help' => 'Help',
+	'search' => 'Search',
+	'top rated' => 'Top rated',
+	'username' => 'Username',
+	'password' => 'Password',
+	'new password' => 'New password',
+	'email' => 'Email',
+	'QQ' => 'QQ',
+	'sex' => 'Sex',
+	'motto' => 'Motto',
+	'rating' => 'Rating',
+	'view all' => 'View all',
+	'appraisal' => 'Appraisal',
+	'submit' => 'Submit',
+	'browse' => 'Browse',
+	'score range' => 'Score range',
+	'details' => 'Details',
+	'hours' => function($h) {
+		return "$h ".($h <= 1 ? 'hour' : 'hours');
+	},
+	'title' => 'Title',
+	'content' => 'Content',
+	'time' => 'Time',
+	'none' => 'None',
+	'user profile' => 'User profile',
+	'send private message' => 'Send private message',
+	'modify my profile' => 'Modify my profile',
+	'visit his blog' => function($name) {
+		return "Visit $name's blog";
+	},
+	'rating changes' => 'Rating changes',
+	'accepted problems' => 'Accepted problems',
+	'n problems in total' => function($n) {
+		return "$n ".($n <= 1 ? 'problem' : 'problems');
+	},
+	'please enter your password for authorization' => 'Please enter your password for authorization',
+	'please enter your new profile' => 'Please enter your new profile',
+	'leave it blank if you do not want to change the password' => 'Leave it blank if you do not want to change the password',
+	'change avatar help' => 'Do you want to change your avatar? Please see <a href="/faq">Help</a>',
+	'enter your username' => 'Enter your username',
+	'enter your email' => 'Enter your email',
+	'enter your password' => 'Enter your password',
+	're-enter your password' => 'Re-enter your password',
+	'enter your new password' => 'Enter your new password',
+	're-enter your new password' => 'Re-enter your new password',
+	'enter your QQ' => 'Enter your QQ',
+	'refuse to answer' => 'Refuse to answer',
+	'male' => 'Male',
+	'female' => 'Female',
+	'server time' => 'Server Time',
+	'opensource project' => 'OpenSource Project'
+];

+ 69 - 0
app/locale/basic/zh-cn.php

@@ -0,0 +1,69 @@
+<?php
+return [
+	'_common_name' => '中文',
+	'login' => '登录',
+	'register' => '注册',
+	'logout' => '登出',
+	'my profile' => '个人信息',
+	'private message' => '私信',
+	'system message' => '系统消息',
+	'system manage' => '系统管理',
+	'contests' => '比赛',
+	'problems' => '题库',
+	'submissions' => '提交记录',
+	'hacks' => 'Hack!',
+	'blogs' => '博客',
+	'announcements' => '公告',
+	'all the announcements' => '所有公告',
+	'help' => '帮助',
+	'search' => '搜索',
+	'top rated' => '比赛排行榜',
+	'username' => '用户名',
+	'password' => '密码',
+	'new password' => '新密码',
+	'email' => 'Email',
+	'QQ' => 'QQ',
+	'sex' => '性别',
+	'motto' => '格言',
+	'rating' => 'Rating',
+	'view all' => '查看全部',
+	'appraisal' => '评价',
+	'submit' => '提交',
+	'browse' => '浏览',
+	'score range' => '分数范围',
+	'details' => '详细',
+	'hours' => function($h) {
+		return "$h 小时";
+	},
+	'title' => '标题',
+	'content' => '内容',
+	'time' => '时间',
+	'none' => '无',
+	'user profile' => '用户信息',
+	'send private message' => '发送私信',
+	'modify my profile' => '更改个人信息',
+	'visit his blog' => function($name) {
+		return "访问 $name 的博客";
+	},
+	'rating changes' => 'Rating 变化',
+	'accepted problems' => 'AC 过的题目',
+	'n problems in total' => function($n) {
+		return "共 $n 道题";
+	},
+	'please enter your password for authorization' => '请输入您的密码进行身份验证',
+	'please enter your new profile' => '请输入新的个人信息',
+	'leave it blank if you do not want to change the password' => '如果不想修改密码请留空',
+	'change avatar help' => '想改头像?见<a href="/faq">帮助</a>',
+	'enter your username' => '输入用户名',
+	'enter your email' => '输入Email',
+	'enter your password' => '输入密码',
+	're-enter your password' => '重新输入密码',
+	'enter your new password' => '输入新密码',
+	're-enter your new password' => '重新输入新密码',
+	'enter your QQ' => '输入QQ',
+	'refuse to answer' => '拒绝回答',
+	'male' => '男',
+	'female' => '女',
+	'server time' => '服务器时间',
+	'opensource project' => '开源项目'
+];

+ 29 - 0
app/locale/contests/en.php

@@ -0,0 +1,29 @@
+<?php
+return [
+	'current or upcoming contests' => 'Current or upcoming contests',
+	'ended contests' => 'Ended contests',
+	'back to the contest' => 'Back to the contest',
+	'contest' => 'Contest',
+	'contest name' => 'Name',
+	'start time' => 'Start time',
+	'duration' => 'Duration',
+	'the number of registrants' => 'Registrants',
+	'register' => 'register',
+	'registered' => 'registered',
+	'in progress' => 'in progress',
+	'pending final test' => 'pending final test',
+	'final testing' => 'final testing',
+	'ended' => 'ended',
+	'add new contest' => 'Add new contest',
+	'contest dashboard' => 'Dashboard',
+	'contest submissions' => 'Submissions',
+	'contest standings' => 'Standings',
+	'contest backstage' => 'Backstage',
+	'contest notice' => 'Notice',
+	'show all submissions' => 'Show all submissions',
+	'contest ends in' => 'Contest ends in',
+	'contest pending final test' => 'Pending final test',
+	'contest final testing' => 'Final testing',
+	'contest ended' => 'Contest Ended',
+	'contest registrants' => 'Registrants'
+];

+ 29 - 0
app/locale/contests/zh-cn.php

@@ -0,0 +1,29 @@
+<?php
+return [
+	'current or upcoming contests' => '正在进行或即将到来的比赛',
+	'ended contests' => '已结束的比赛',
+	'back to the contest' => '返回比赛',
+	'contest' => '比赛',
+	'contest name' => '比赛名称',
+	'start time' => '开始时间',
+	'duration' => '时长',
+	'the number of registrants' => '报名人数',
+	'register' => '报名',
+	'registered' => '已报名',
+	'in progress' => '正在进行',
+	'pending final test' => '等待评测',
+	'final testing' => '正在评测',
+	'ended' => '已结束',
+	'add new contest' => '添加比赛',
+	'contest dashboard' => '比赛主页',
+	'contest submissions' => '提交记录',
+	'contest standings' => '排行榜',
+	'contest backstage' => '大后台',
+	'contest notice' => '比赛通知',
+	'show all submissions' => '显示所有提交',
+	'contest ends in' => '距离比赛结束',
+	'contest pending final test' => '等待评测',
+	'contest final testing' => '正在测评',
+	'contest ended' => '比赛已结束',
+	'contest registrants' => '报名选手列表'
+];

+ 47 - 0
app/locale/problems/en.php

@@ -0,0 +1,47 @@
+<?php
+return [
+	'problem' => 'Problem',
+	'all problems' => 'All Problems',
+	'template problems' => 'Template Problems',
+	'add new' => 'Add new problem',
+	'title' => 'Title',
+	'ac' => 'AC',
+	'submit' => 'Submit',
+	'ac rate' => 'AC Rate',
+	'show tags' => 'Show tags',
+	'show statistics' => 'Show statistics',
+	'statement' => 'Statement',
+	'custom test' => 'Custom Test',
+	'manage' => 'Manage',
+	'statistics' => 'Statistics',
+	'run' => 'Run',
+	'source code' => 'Source code',
+	'text file' => 'Text file',
+	'zip file upload introduction' => function($str) {
+		return "Upload a .zip file containing $str:";
+	},
+	'or upload files one by one' => 'or upload files one by one:',
+	'accepted submissions' => 'Accepted Submissions',
+	'fastest' => 'fastest',
+	'shortest' => 'shortest',
+	'score distribution' => 'Score distribution',
+	'prefix sum of score distribution' => 'Prefix sum',
+	'suffix sum of score distribution' => 'Suffix sum',
+	'submitter' => 'Submitter',
+	'hacker' => 'Hacker',
+	'owner' => 'Owner',
+	'result' => 'Result',
+	'used time' => 'Time',
+	'used memory' => 'Memory',
+	'language' => 'Language',
+	'file size' => 'File size',
+	'submit time' => 'Submit time',
+	'judge time' => 'Judge time',
+	'problem id' => 'Problem ID',
+	'submission' => 'Submission',
+	'hack' => 'Hack',
+	'submission id' => 'Submission ID',
+	'my submissions' => 'My Submissions',
+	'hacks by me' => 'Hacks by me',
+	'hacks to me' => 'Hacks to me'
+];

+ 47 - 0
app/locale/problems/zh-cn.php

@@ -0,0 +1,47 @@
+<?php
+return [
+	'problem' => '题目',
+	'all problems' => '总题库',
+	'template problems' => '模板题库',
+	'add new' => '添加新题',
+	'ac' => 'AC',
+	'submit' => '提交',
+	'ac rate' => 'AC 率',
+	'show tags' => '显示标签',
+	'show statistics' => '显示提交情况',
+	'statement' => '描述',
+	'custom test' => '自定义测试',
+	'manage' => '管理',
+	'submissions statistics' => '统计提交情况',
+	'statistics' => '统计',
+	'run' => '运行',
+	'source code' => '源文件',
+	'text file' => '文本文件',
+	'zip file upload introduction' => function($str) {
+		return "上传一个zip压缩文件,包含 {$str}:";
+	},
+	'or upload files one by one' => '或者逐个上传:',
+	'accepted submissions' => '满分提交',
+	'fastest' => '最快',
+	'shortest' => '最短',
+	'score distribution' => '得分分布',
+	'prefix sum of score distribution' => '前缀和',
+	'suffix sum of score distribution' => '后缀和',
+	'submitter' => '提交者',
+	'hacker' => 'Hacker',
+	'owner' => 'Owner',
+	'result' => '结果',
+	'used time' => '用时',
+	'used memory' => '内存',
+	'language' => '语言',
+	'file size' => '文件大小',
+	'submit time' => '提交时间',
+	'judge time' => '测评时间',
+	'problem id' => '题目ID',
+	'submission' => '提交记录',
+	'hack' => 'Hack',
+	'submission id' => '提交记录ID',
+	'my submissions' => '我的提交记录',
+	'hacks by me' => '我的Hack记录',
+	'hacks to me' => '我的被Hack记录'
+];

+ 83 - 0
app/models/Auth.php

@@ -0,0 +1,83 @@
+<?php
+
+class Auth {
+	public static function check() {
+		global $myUser;
+		return $myUser != null;
+	}
+	public static function id() {
+		global $myUser;
+		return $myUser['username'];
+	}
+	public static function user() {
+		global $myUser;
+		return $myUser;
+	}
+	public static function login($username, $remember = true) {
+		if (!validateUsername($username)) {
+			return;
+		}
+		$_SESSION['username'] = $username;
+		if ($remember) {
+			$remember_token = DB::selectFirst("select remember_token from user_info where username = '$username'")['remember_token'];
+			if ($remember_token == '') {
+				$remember_token = uojRandString(60);
+				DB::update("update user_info set remember_token = '$remember_token' where username = '$username'");
+			}
+
+			$expire = time() + 60 * 60 * 24 * 365 * 10;
+			Cookie::safeSet('uoj_username', $username, $expire, '/', array('httponly' => true));
+			Cookie::safeSet('uoj_remember_token', $remember_token, $expire, '/', array('httponly' => true));
+		}
+	}
+	public static function logout() {
+		unset($_SESSION['username']);
+		unset($_SESSION['last_visited']);
+		Cookie::safeUnset('uoj_username', '/');
+		Cookie::safeUnset('uoj_remember_token', '/');
+		DB::update("update user_info set remember_token = '' where username = '".Auth::id()."'");
+	}
+
+	private static function initMyUser() {
+		global $myUser;
+		$myUser = null;
+		
+		Cookie::safeCheck('uoj_username', '/');
+		Cookie::safeCheck('uoj_remember_token', '/');
+		
+		if (isset($_SESSION['username'])) {
+			if (!validateUsername($_SESSION['username'])) {
+				return;
+			}
+			$myUser = queryUser($_SESSION['username']);
+			return;
+		}
+
+		$remember_token = Cookie::safeGet('uoj_remember_token', '/');
+		if ($remember_token != null) {
+			$username = Cookie::safeGet('uoj_username', '/');
+			if (!validateUsername($username)) {
+				return;
+			}
+			$myUser = queryUser($username);
+			if ($myUser['remember_token'] !== $remember_token) {
+				$myUser = null;
+			}
+			return;
+		}
+	}
+	public static function init() {
+		global $myUser;
+		
+		Auth::initMyUser();
+		if ($myUser) {
+			if ($myUser['usergroup'] == 'B') {
+				$myUser = null;
+			}
+		}
+		if ($myUser) {
+			DB::update("update user_info set remote_addr = '".DB::escape($_SERVER['REMOTE_ADDR'])."', http_x_forwarded_for = '".DB::escape($_SERVER['HTTP_X_FORWARDED_FOR'])."' where username = '".DB::escape($myUser['username'])."'");
+			$_SESSION['last_visited'] = time();
+		}
+	}
+}

+ 46 - 0
app/models/Cookie.php

@@ -0,0 +1,46 @@
+<?php
+
+class Cookie {
+	public static function checksum($key, $val) {
+		return md5(UOJConfig::$data['security']['cookie']['checksum_salt'][0] . $key . UOJConfig::$data['security']['cookie']['checksum_salt'][1] . $val . UOJConfig::$data['security']['cookie']['checksum_salt'][2]);
+	}
+	
+	public static function get($key) {
+		return isset($_COOKIE[$key]) ? $_COOKIE[$key] : null;
+	}
+	public static function set($key, $val, $expire = 0, $path = null, $config = array()) {
+		$config = array_merge(array(
+			'secure' => false,
+			'httponly' => false
+		), $config);
+		$_COOKIE[$key] = $val;
+		return setcookie($key, $val, $expire, $path, UOJContext::cookieDomain(), $config['secure'], $config['httponly']);
+	}
+	public static function unsetVar($key, $path = null) {
+		if (!isset($_COOKIE[$key])) {
+			return true;
+		}
+		unset($_COOKIE[$key]);
+		return setcookie($key, null, -1, $path, UOJContext::cookieDomain());
+	}
+	public static function safeCheck($key, $path = null) {
+		if (!isset($_COOKIE[$key])) {
+			return;
+		}
+		if (!isset($_COOKIE[$key . '_checksum']) || $_COOKIE[$key . '_checksum'] !== Cookie::checksum($key, $_COOKIE[$key])) {
+			Cookie::safeUnset($key, $path);
+		}
+	}
+	public static function safeUnset($key, $path = null) {
+		Cookie::unsetVar($key, $path);
+		Cookie::unsetVar($key . '_checksum', $path);
+	}
+	public static function safeGet($key, $path = null) {
+		Cookie::safeCheck($key, $path);
+		return Cookie::get($key);
+	}
+	public static function safeSet($key, $val, $expire = 0, $path = null, $config = array()) {
+		Cookie::set($key, $val, $expire, $path, $config);
+		Cookie::set($key . '_checksum', Cookie::checksum($key, $val), $expire, $path, $config);
+	}
+}

+ 78 - 0
app/models/DB.php

@@ -0,0 +1,78 @@
+<?php
+
+class DB {
+	public static function init() {
+		global $uojMySQL;
+		@$uojMySQL = mysqli_connect(UOJConfig::$data['database']['host'] . ':3306', UOJConfig::$data['database']['username'], UOJConfig::$data['database']['password'], UOJConfig::$data['database']['database']);
+		if (!$uojMySQL) {
+			echo 'There is something wrong with database >_<.... ' . mysqli_connect_error();
+			die();
+		}
+	}
+	public static function escape($str) {
+		global $uojMySQL;
+		return mysqli_real_escape_string($uojMySQL, $str);
+	}
+	public static function fetch($r, $opt = MYSQLI_ASSOC) {
+		global $uojMySQL;
+		return mysqli_fetch_array($r, $opt);
+	}
+	
+	public static function query($q) {
+		global $uojMySQL;
+		return mysqli_query($uojMySQL, $q);
+	}
+	public static function update($q) {
+		global $uojMySQL;
+		return mysqli_query($uojMySQL, $q);
+	}
+	public static function insert($q) {
+		global $uojMySQL;
+		return mysqli_query($uojMySQL, $q);
+	}
+	public static function insert_id() {
+		global $uojMySQL;
+		return mysqli_insert_id($uojMySQL);
+	}
+		
+	public static function delete($q) {
+		global $uojMySQL;
+		return mysqli_query($uojMySQL, $q);
+	}
+	public static function select($q) {
+		global $uojMySQL;
+		return mysqli_query($uojMySQL, $q);
+	}
+	public static function selectAll($q, $opt = MYSQLI_ASSOC) {
+		global $uojMySQL;
+		$res = array();
+		$qr = mysqli_query($uojMySQL, $q);
+		while ($row = mysqli_fetch_array($qr, $opt)) {
+			$res[] = $row;
+		}
+		return $res;
+	}
+	public static function selectFirst($q, $opt = MYSQLI_ASSOC) {
+		global $uojMySQL;
+		return mysqli_fetch_array(mysqli_query($uojMySQL, $q), $opt);
+	}
+	public static function selectCount($q) {
+		global $uojMySQL;
+		list($cnt) = mysqli_fetch_array(mysqli_query($uojMySQL, $q), MYSQLI_NUM);
+		return $cnt;
+	}
+	
+	public static function checkTableExists($name) {
+		global $uojMySQL;
+		return DB::query("select 1 from $name") !== false;
+	}
+	
+	public static function num_rows() {
+		global $uojMySQL;
+		return mysqli_num_rows($uojMySQL);
+	}
+	public static function affected_rows() {
+		global $uojMySQL;
+		return mysqli_affected_rows($uojMySQL);
+	}
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio