此文档只会介绍 web 中各部分的功能,但不会介绍具体细节。如需了解具体细节,请查看源代码及其中的注释。
Web 模块相关代码位于 web/ 目录下。本文档中,除非特殊说明,否则所有涉及的文件都以 web 目录为基础。
Web 模块负责处理所有用户操作,包括查看数据、修改个人资料、上传数据、提交代码等操作。
用户数据、题面数据、提交数据、比赛数据等均储存在 PostgreSQL 数据库中(参见 PostgreSQL 数据库章节),并由 Web 模块管理。
Web 模块使用 Flask 渲染网页、处理 HTTP 请求。
Web 模块使用 Redis 缓存数据、中转数据,关于具体内容,参见 Redis 文档。
关于如何修改数据库,请参阅修改数据库章节。
如需了解更多细节,请参阅 Flask 文档。
每个请求均会调用相应的函数(由函数前的修饰器指定,如 @web.route('/index.html') 表示对 /OnlineJudge/index.html 的请求)。
web.py 负责处理所有除 /admin 及其子页面以外的请求;admin.py 负责处理所有 /admin 及其子页面的请求。
Cross-Site Request Forgery (简称 CSRF 或 XSRF) 是一类比较常见的网络攻击方式。攻击者诱导用户访问一个第三方网页,第三方网页会向 OJ 发起请求。由于早期浏览器同源政策不是很规范,导致 GET 或 POST 请求都可以直接由其他域名发起,无需用户确认,且请求中会携带用户的 cookies。这种攻击方式下,第三方网页无法获取到生成的请求返回的响应,攻击者的目的是请求所带来的副作用。
OJ 对 GET 请求没有做防护,根据 HTTP 的语义,GET 请求不应该产生状态的改变。因此,即使请求没有参数,只要这个请求会写数据库 (写 log 等信息除外),就不应该使用 GET 方法。
OJ 对 所有 POST 请求都有强制的 CSRF 保护,无声明的 POST 请求会被直接拦截。相关代码位于 web/csrf.py 中。可以用两种方式来证明一个请求不是 CSRF,满足其中一种即会放行:
- 如果是用 JS 发起请求:在 HTTP 头中将
X-Acmoj-Is-Csrf设置为除yes外的任意值 (建议设置为no)。CSRF 的攻击请求无法设置 HTTP 头。使用 JQuery.ajax 或 window.fetch 时无需手动设置请求头,OJ 页面框架中针对这两个 API 做了特殊设置,发起 POST 请求时会自动设置请求头,相关代码位于static/js/csrf.js。使用XMLHttpRequest时需手动设置请求头。 - 如果是 HTML 中的表单提交:在 Jinja template 中
<form>里包含一个{{ g.csrf() }}。这个函数会返回一个带有 CSRF token 的<input type="hidden">,token 同时存储于名为__Host-acmoj-csrf的 cookie 中。CSRF 的攻击请求无法获取也无法设置__Host-开头的 cookie。
除 GET 和 POST 外,PUT、DELETE 等其他 HTTP verb 无法跨域发起,故无需保护。
所有页面的渲染均由 render_template 函数 (templating.py) 完成。此函数会将 web/templates/ 目录下的模板文件按照特定规则渲染出来。关于渲染规则,请参阅 Flask 文档中的 Rendering Templates 部分。
另请注意,web/templates/ 目录下的模板文件与网页的 URL 无关,而是由 render_template 函数的第一个参数决定。
更改 docs/user 下的文档后,需要手动运行 make user-docs 生成 HTML 格式的用户文档。这一过程需要安装 Pandoc 工具。
除了静态内容(如图片、CSS、JS 等)和发送的心跳包以外,所有前端请求均会被记录,config.py 文件下 LogConfig 类中的配置决定日志记录的方式、位置。
登录管理由 session_manager.py 负责,可以提供真实登录状态、用户名、昵称、权限,及新增登录用户。登录状态数据由 Redis 管理,Redis 数据库中前缀包含 session_ 的键均为登录状态。数据库中的数据的有效时间为 14 天。
用户数据由 user_manager.py 负责。其可以创建用户、修改用户、删除用户、修改用户密码、获取用户数据、获取用户列表。用户数据由 PostgreSQL 管理(参见 PostgreSQL 数据库的用户数据表章节)。
在用户数据管理中,用户名 (username) 是唯一且不变的。
为尽可能确保用户密码安全,用户密码将会加上一个随机生成的 salt,然后使用 Argon2 算法加密。加密后的密码将会存储在数据库中。
用户权限共有 3 种,
0: 普通用户,可以修改自己的个人资料、提交代码、查看比赛数据等,不可以修改他人的个人资料、查看他人的非公开提交;1: 已弃用;2: 超级管理员,除管理员权限外,可以修改他人的个人资料。
班级管理由 course_manager.py 负责。其可以管理班级数据。班级数据由 PostgreSQL 管理(参见 PostgreSQL 数据库的班级数据表章节)。
当用户加入班级后,班级的管理员就可以管理该用户,包括查看其全部提交、布置作业。
当班级管理员将用户设置为成员,且用户加入班级,才能正常统计作业情况;如果没有设置为成员,则统计时不会包括该用户。(这里认为没有设置为「为所有用户排名」)
特别地,如果管理员所在班级被标记为 site_owner,则其可以查看所有提交、创建比赛、修改比赛、删除比赛、新增题目、修改题目数据、删除题目,但不能修改他人的个人资料。
关于比赛管理,参见比赛管理章节。
关于班级分组,参见班级分组章节。
关于题目权限,参见题目权限章节。
每一个比赛都有其对应的班级。当管理员属于当前比赛所对应的班级时,其拥有比赛管理权限。
一个班级可以设置若干分组。分组唯一的作用在于分配需要完成的作业、比赛、考试。在权限管理方面,分组不会产生任何影响。
题目权限有:
- 只读权限:在发布时间之前查看问题;下载题目数据包;查看本题目下任意用户提交的代码及评测状态。
- 完整权限:所有只读权限;修改题面;修改题目数据;重测提交;将提交标记为无效;删除题目。
每一个题目对应一个权限。当管理员属于当前题目所对应的班级时,其自动拥有完整权限。当管理员属于当前题目所对应的班级标签时,其自动拥有只读权限。
除此以外,用户可以主动为他人添加权限。
实名数据由 realname_manager.py 负责。其可以修改实名、获取实名数据。实名数据由 PostgreSQL 管理(参见 PostgreSQL 数据库的实名数据表章节)。
比赛数据(这里比赛是广泛意义上的比赛,作业、考试、比赛都包含在内)由 contest_manager.py 负责。其可以创建比赛、修改比赛、删除比赛、修改参赛用户、获取比赛数据、获取比赛列表。比赛数据由 PostgreSQL 管理(参见 PostgreSQL 数据库的比赛数据表、比赛题目表、比赛用户表章节)。
比赛类型共有 3 种,
0: 比赛,统计每题得分及罚时,并据此排序;1: 作业,仅统计通过和不通过的提交;2: 考试,同一时刻非管理员用户只能报名同一场考试,在考试结束前暂时无法加入新的作业、比赛或考试,考试时非管理员用户只能查看自己在考试题目上的提交。
比赛数据缓存由 contest_cache.py 负责,以前缀 ranking-{type}-{id} 存入 Redis 数据库,此缓存数据有效时间为 14s。
讨论数据由 discuss_manager.py 负责。其可以创建讨论、修改讨论、删除讨论、获取讨论数据。讨论数据由 PostgreSQL 管理(参见 PostgreSQL 数据库的讨论数据表章节)。
题目数据由 problem_manager.py 负责。其可以创建题目、修改题目、删除题目、获取题目数据、获取题目列表。题目数据中与题面相关的部分由 PostgreSQL 管理(参见 PostgreSQL 数据库的题目数据表章节)。题目数据中的数据包由 S3 服务管理,具体参见 S3 服务文档。
此外,QuizManager.py 可以从 S3 服务上获取 json 数据。
当数据包有更新(或首次上传)时,此信息将会通过网络的形式传给 Scheduler2 模块。
关于数据包的格式,参见用户文档中的数据包格式。
评测数据由 judge_manager.py 负责,其可以查询提交、为尚未评测的提交交给 Scheduler2 模块排程、中止 (abort) 评测、将评测标记为无效 (voided)、重新评测提交。数据参见 PostgreSQL 数据库的评测数据表章节。
旧版评测数据由 old_judge_manager.py 负责。由于架构的不兼容性,目前仅可以查询提交,数据视为只读(数据参见 PostgreSQL 数据库的旧评测数据表章节)。
PostgreSQL 数据库与 common/models.py 中 Base 的派生类对应。
每个表对应一个类,每个字段对应一个类的成员(表示关系的成员除外,这类成员是计算得出的)。
数据库版本由 alembic 管理,每次修改数据库时,都会生成一个 migration script,然后将其应用到数据库。
如需修改数据库,请先修改 models.py,然后执行(请将 message 替换为此次更新的描述)
alembic revision --autogenerate -m "message"alembic 会自动对比数据库内容和当前 model 的差异,生成 migration script。
关于将修改应用到数据库,请参阅迁移文档的 Web#迁移数据库部分。
另请参见用户数据管理章节。
id: 用户 id;username: 用户名;username_lower: 用户名的小写形式;student_id: 学工号;friendly_name: 昵称;password: 密码;privilege: 用户权限;data_license_agreed: 是否同意数据使用协议。
另请参见班级权限管理章节。
id: 课程标签 id;name: 课程标签名称。site_owner: 如果此项为 true,则属于这个 tag 中任一班级的管理员可以查看所有提交、创建比赛、修改比赛、删除比赛、新增题目、修改题目数据、删除题目。
另请参见班级权限管理章节。
id: 学期 id;name: 学期名称;start_time: 学期开始时间;end_time: 学期结束时间。
另请参见班级权限管理章节。
id: 课程 id;name: 课程名称;description: 课程描述;tag_id: 课程所属标签 id;term_id: 课程所属学期 id。
另请参见班级权限管理章节。
id: 条目 id;user_id: 用户 id;course_id: 课程 id;admin: 是否是管理员。
另请参见班级分组章节。
id: 条目 id;name: 分组名称;description: 分组描述;course_id: 分组所属课程 id。
另请参见实名数据管理章节。
id: 条目 id;student_id: 学工号;real_name: 实名;course_id: 所属课程 id。
另请参见班级分组章节。
id: 条目 id;group_id: 分组 id;realname_reference_id: 实名 id。
比赛、考试和作业都是一种 contest。
另请参见比赛数据管理章节。
id: 比赛 id;name: 比赛名称;start_time: 比赛开始时间;end_time: 比赛结束时间;type: 比赛类型;ranked: 是否显示排名;rank_penalty: 是否计算罚时;rank_partial_score: 是否计算部分分;description: 比赛描述;course_id: 所属课程 id;group_ids: 所属分组 id 列表;completion_criteria: 比赛要求(如完成题目数量或需达到的分数);allow_languages: 允许提交的语言;rank_all_users: 是否为所有用户排名。
另请参见比赛数据管理章节。
id: 条目 id;contest_id: 题目所属比赛 id;problem_id: 题目 id。
另请参见比赛数据管理章节。
id: 条目 id;contest_id: 用户所属比赛 id;user_id: 用户 id。
另请参见讨论数据管理章节。
id: 讨论 id;problem_id: 题目 id;user_id: 用户 id;data: 讨论内容。
此表仅包含特别赋予权限的情况,属于本班级的管理员不会出现在此表中。
另请参见题目权限章节。
id: 条目 id;user_id: 用户 id;problem_id: 题目 id;privilege: 用户权限(只读/全部);comment: 备注。
另请参见题目数据管理章节。
id: 题目 id;title: 题目标题;description: 题目描述;input: 输入格式;output: 输出格式;examples: 样例(新版);example_input: 样例输入;example_output: 样例输出;data_range: 数据范围;release_time: 发布时间;problem_type: 题目类型;limits: 时空限制;course_id: 所属课程 id。
在 Judger2 架构下,judge_record_v1 表是只读的,仅用于记录过去的提交数据。
另请参见评测数据管理章节。
id: 提交 id;code: 提交代码;user_id: 提交用户的 id;problem_id: 提交题目;language: 提交语言;status: 评测状态;score: 评测得分;time: 提交时间;time_msecs: 评测耗时;memory_kbytes: 评测内存占用;detail: 评测详情;public: 是否公开。
另请参见评测数据管理章节。
id: 提交 ID;public: 是否公开;language: 提交语言;user_id: 提交用户的 id;problem_id: 提交题目;status: 评测状态;score: 评测得分;message: 评测详情信息;details: 评测详情;time_msecs: 评测耗时 (ms);memory_bytes: 评测内存占用 (bytes)。
id: 评测机 ID;name: 评测机名称;hardware: 评测机硬件信息;provider: 评测机提供者;visible: 是否显示在关于页上。
id: 评测机 id;address: 评测机 ip 地址;secret_key: 评测机密钥;last_seen_time: 最后一次心跳时间;busy: 评测机是否正在评测;current_task: 评测机正在评测的提交 id;friendly_name: 评测机名称;detail: 评测机硬件信息。
version_num: 版本号,用于记录数据库格式的版本。
- data 数组的第一维代表一个参赛用户,从 0 开始。
- data[i]['score'] 存储第 i 个用户的总得分。
- data[i]['penalty'] 存储第 i 个用户的总罚时。
- data[i]['ac_count'] 存储第 i 个用户的 AC 数量。
- data[i]['friendly_name'] 存储第 i 个用户的昵称。
- data[i]['problems'] 存储第 i 个用户各题的提交与通过情况。其中,
- data[i]['problems'][j]['score'] 存储第 j 道题的得分。
- data[i]['problems'][j]['count'] 存储第 j 道题的提交次数。
- data[i]['problems'][j]['accepted'] 存储第 j 道题通过与否。
- data[i]['realname'] 存储第 i 个用户的真实姓名(Realname reference)。
- data[i]['student_id'] 存储第 i 个用户的学工号。
- data[i]['username'] 存储第 i 个用户的用户名。