diff --git a/config-example.json b/config-example.json index 61a0929..c3df2b3 100644 --- a/config-example.json +++ b/config-example.json @@ -20,6 +20,11 @@ "show": true } }, + "limit": { + "time_limit": 10000, + "memory_limit": 1024, + "data_size": 209715200 + }, "page": { "problem": 50, "problem_statistics": 10, diff --git a/models/problem.js b/models/problem.js index 61d9d34..5f9eeb7 100644 --- a/models/problem.js +++ b/models/problem.js @@ -242,11 +242,14 @@ class Problem extends Model { } async updateTestdata(path) { - let fs = Promise.promisifyAll(require('fs')); + let fs = Promise.promisifyAll(require('fs-extra')); let buf = await fs.readFileAsync(path); + + if (buf.length > syzoj.config.limit.data_size) throw 'Testdata too large.' + let key = syzoj.utils.md5(buf); - await fs.rename(path, TestData.resolvePath(key)); + await fs.moveAsync(path, TestData.resolvePath(key), { overwrite: true }); if (this.testdata_id) { let tmp = this.testdata_id; @@ -262,6 +265,26 @@ class Problem extends Model { }); await file.save(); this.testdata_id = file.id; + + await this.save(); + } + + async validate() { + if (this.time_limit <= 0) return 'Invalid time limit'; + if (this.time_limit > syzoj.config.limit.time_limit) return 'Time limit too large'; + if (this.memory_limit <= 0) return 'Invalid memory limit'; + if (this.memory_limit > syzoj.config.limit.memory_limit) return 'Memory limit too large'; + + let filenameRE = /^[\w \-\+\.]*$/; + if (this.file_io_input_name && !filenameRE.test(this.file_io_input_name)) return 'Invalid input file name'; + if (this.file_io_output_name && !filenameRE.test(this.file_io_output_name)) return 'Invalid output file name'; + + if (this.file_io) { + if (!this.file_io_input_name) return 'No input file name'; + if (!this.file_io_output_name) return 'No output file name'; + } + + return null; } async getJudgeState(user, acFirst) { @@ -371,6 +394,33 @@ class Problem extends Model { return res; } + async setTags(newTagIDs) { + let ProblemTagMap = syzoj.model('problem_tag_map'); + + let oldTagIDs = (await this.getTags()).map(x => x.id); + + let delTagIDs = oldTagIDs.filter(x => !newTagIDs.includes(x)); + let addTagIDs = newTagIDs.filter(x => !oldTagIDs.includes(x)); + + for (let tagID of delTagIDs) { + let map = await ProblemTagMap.findOne({ where: { + problem_id: this.id, + tag_id: tagID + } }); + + await map.destroy(); + } + + for (let tagID of addTagIDs) { + let map = await ProblemTagMap.create({ + problem_id: this.id, + tag_id: tagID + }); + + await map.save(); + } + } + getModel() { return model; } } diff --git a/modules/problem.js b/modules/problem.js index c989ebb..eb00df0 100644 --- a/modules/problem.js +++ b/modules/problem.js @@ -126,6 +126,38 @@ app.get('/problem/:id', async (req, res) => { } }); +app.get('/problem/:id/export', async (req, res) => { + try { + let id = parseInt(req.params.id); + let problem = await Problem.fromID(id); + if (!problem || !problem.is_public) throw 'No such problem.'; + + let obj = { + title: problem.title, + description: problem.description, + input_format: problem.input_format, + output_format: problem.output_format, + example: problem.example, + limit_and_hint: problem.limit_and_hint, + time_limit: problem.time_limit, + memory_limit: problem.memory_limit, + file_io: problem.file_io, + file_io_input_name: problem.file_io_input_name, + file_io_output_name: problem.file_io_output_name, + tags: [] + }; + + let tags = await problem.getTags(); + + obj.tags = tags.map(tag => tag.name); + + res.send({ success: true, obj: obj }); + } catch (e) { + syzoj.log(e); + res.send({ success: false, error: e }); + } +}); + app.get('/problem/:id/edit', async (req, res) => { try { let id = parseInt(req.params.id) || 0; @@ -156,7 +188,7 @@ app.get('/problem/:id/edit', async (req, res) => { app.post('/problem/:id/edit', async (req, res) => { try { - let id = parseInt(req.params.id); + let id = parseInt(req.params.id) || 0; let problem = await Problem.fromID(id); if (!problem) { problem = await Problem.create(); @@ -183,28 +215,97 @@ app.post('/problem/:id/edit', async (req, res) => { req.body.tags = [req.body.tags]; } - let oldTagIDs = (await problem.getTags()).map(x => x.id); let newTagIDs = await req.body.tags.map(x => parseInt(x)).filterAsync(async x => ProblemTag.fromID(x)); + await problem.setTags(newTagIDs); - let delTagIDs = oldTagIDs.filter(x => !newTagIDs.includes(x)); - let addTagIDs = newTagIDs.filter(x => !oldTagIDs.includes(x)); + res.redirect(syzoj.utils.makeUrl(['problem', problem.id])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); - for (let tagID of delTagIDs) { - let map = await ProblemTagMap.findOne({ where: { - problem_id: id, - tag_id: tagID - } }); +app.get('/problem/:id/import', async (req, res) => { + try { + let id = parseInt(req.params.id) || 0; + let problem = await Problem.fromID(id); + if (!problem) { + problem = await Problem.create(); + problem.id = id; + problem.user_id = res.locals.user.id; + } else { + if (!await problem.isAllowedUseBy(res.locals.user)) throw 'Permission denied.'; + if (!await problem.isAllowedEditBy(res.locals.user)) throw 'Permission denied.'; + } - await map.destroy(); + res.render('problem_import', { + problem: problem + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.post('/problem/:id/import', async (req, res) => { + try { + let id = parseInt(req.params.id) || 0; + let problem = await Problem.fromID(id); + if (!problem) { + problem = await Problem.create(); + if (id) problem.id = id; + problem.user_id = res.locals.user.id; + } else { + if (!await problem.isAllowedUseBy(res.locals.user)) throw 'Permission denied.'; + if (!await problem.isAllowedEditBy(res.locals.user)) throw 'Permission denied.'; } - for (let tagID of addTagIDs) { - let map = await ProblemTagMap.create({ - problem_id: id, - tag_id: tagID - }); + let request = require('request-promise'); + let url = require('url'); + + let json = await request({ + uri: url.resolve(req.body.url, 'export'), + timeout: 1500, + json: true + }); + + if (!json.success) throw `Failed to load problem: ${json.error}`; + + problem.title = json.obj.title; + problem.description = json.obj.description; + problem.input_format = json.obj.input_format; + problem.output_format = json.obj.output_format; + problem.example = json.obj.example; + problem.limit_and_hint = json.obj.limit_and_hint; + problem.time_limit = json.obj.time_limit; + problem.memory_limit = json.obj.memory_limit; + problem.file_io = json.obj.file_io; + problem.file_io_input_name = json.obj.file_io_input_name; + problem.file_io_output_name = json.obj.file_io_output_name; + + let validateMsg = await problem.validate(); + if (validateMsg) throw 'Invalid problem: ' + validateMsg; - await map.save(); + await problem.save(); + + let tagIDs = (await json.obj.tags.mapAsync(name => ProblemTag.findOne({ where: { name: name } }))).filter(x => x).map(tag => tag.id); + await problem.setTags(tagIDs); + + let download = require('download'); + let tmp = require('tmp-promise'); + let tmpFile = await tmp.file(); + let fs = require('bluebird').promisifyAll(require('fs')); + + try { + let data = await download(url.resolve(req.body.url, 'download')); + await fs.writeFileAsync(tmpFile.path, data); + await problem.updateTestdata(tmpFile.path); + } catch (e) { + syzoj.log(e); } res.redirect(syzoj.utils.makeUrl(['problem', problem.id])); @@ -252,6 +353,10 @@ app.post('/problem/:id/data', app.multer.single('testdata'), async (req, res) => problem.file_io = req.body.io_method === 'file-io'; problem.file_io_input_name = req.body.file_io_input_name; problem.file_io_output_name = req.body.file_io_output_name; + + let validateMsg = await problem.validate(); + if (validateMsg) throw 'Invalid problem: ' + validateMsg; + if (req.file) { await problem.updateTestdata(req.file.path); } @@ -330,9 +435,12 @@ app.get('/problem/:id/download', async (req, res) => { await problem.loadRelationships(); + if (!problem.testdata) throw 'No testdata'; + res.download(problem.testdata.getPath(), `testdata_${id}.zip`); } catch (e) { syzoj.log(e); + res.status(404); res.render('error', { err: e }); diff --git a/package.json b/package.json index a60aa24..4ba22ba 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,12 @@ "ansi-to-html": "^0.4.2", "body-parser": "^1.15.2", "cookie-parser": "^1.4.3", + "download": "^5.0.3", "ejs": "^2.5.2", "express": "^4.14.0", "express-session": "^1.14.1", "file-size": "^1.0.0", + "fs-extra": "^2.1.2", "gravatar": "^1.5.2", "moemark-renderer": "^1.2.5", "moment": "^2.15.0", @@ -42,6 +44,7 @@ "sequelize": "^3.24.3", "session-file-store": "^1.0.0", "sqlite3": "^3.1.4", - "syzoj-divine": "^1.0.0" + "syzoj-divine": "^1.0.0", + "tmp-promise": "^1.0.3" } } diff --git a/views/problem_import.ejs b/views/problem_import.ejs new file mode 100644 index 0000000..45b20a5 --- /dev/null +++ b/views/problem_import.ejs @@ -0,0 +1,16 @@ +<% this.title = '导入题目'; %> +<% include header %> +
+

导入题目

+
+
+ + +
+
+ + +
+ +
+<% include footer %> diff --git a/views/problems.ejs b/views/problems.ejs index a5aed7a..a78ab3d 100644 --- a/views/problems.ejs +++ b/views/problems.ejs @@ -8,7 +8,7 @@ if (typeof tags !== 'undefined') tagIDs = tags.map(x => x.id);
-
+
<% if (typeof tags !== 'undefined') { %> <% tags.sort((a, b) => { @@ -24,7 +24,7 @@ if (typeof tags !== 'undefined') tagIDs = tags.map(x => x.id); <% } %> <% } %>
-
+
<% include footer %>