├── .gitignore ├── routes ├── skin_settings.js ├── random.js ├── reset_xref.js ├── blame.js ├── diff.js ├── router.js ├── raw.js ├── randompage.js ├── contribution_discuss.js ├── sidebar.js ├── star.js ├── delete.js ├── revert.js ├── login_history.js ├── preview.js ├── contribution_document.js ├── suspend_account.js ├── delete_account.js ├── session.js ├── recent_discuss.js ├── search.js ├── grant.js ├── block_history.js ├── move.js ├── backlink.js ├── license.js ├── public_api.js └── change_username.js ├── backend ├── unfinished │ ├── random.js │ ├── blame.js │ ├── diff.js │ ├── raw.js │ ├── randompage.js │ ├── contribution_discuss.js │ ├── star.js │ ├── recent_changes.js │ ├── delete.js │ ├── revert.js │ ├── login_history.js │ ├── preview.js │ ├── grant.js │ ├── contribution_document.js │ ├── search.js │ ├── suspend_account.js │ ├── delete_account.js │ ├── recent_discuss.js │ ├── session.js │ ├── history.js │ ├── move.js │ ├── block_history.js │ ├── backlink.js │ ├── license.js │ ├── change_username.js │ ├── upload.js │ └── special_pages.js ├── backend.js └── wiki.js ├── namumark_parser_multithreaded.js ├── hostconfig.js ├── database.js ├── frontends └── nuxt │ └── frontend.js ├── LICENSE ├── namumark.js ├── package.json ├── fileserver.js ├── undelete-thread.js ├── backlink-reset.js ├── search.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | skins/ 3 | wikidata.db 4 | config.json 5 | node.exe 6 | js/ 7 | !js/banana.js 8 | css/ 9 | -------------------------------------------------------------------------------- /routes/skin_settings.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/settings$/, async(req, res) => { 2 | res.send(await render(req, '스킨 설정', '이 스킨은 설정 기능을 지원하지 않습니다.', {}, _, _, 'settings')); 3 | }); -------------------------------------------------------------------------------- /routes/random.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/random$/, async(req, res) => { 2 | var data = await curs.execute("select title from documents where namespace = '문서' order by random() limit 1"); 3 | if(!data.length) res.redirect('/'); 4 | res.redirect('/w/' + encodeURIComponent(data[0].title)); 5 | }); -------------------------------------------------------------------------------- /backend/unfinished/random.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/random$/, async(req, res) => { 2 | var data = await curs.execute("select title from documents where namespace = '문서' order by random() limit 1"); 3 | if(!data.length) res.redirect('/'); 4 | res.redirect('/w/' + encodeURIComponent(data[0].title)); 5 | }); -------------------------------------------------------------------------------- /namumark_parser_multithreaded.js: -------------------------------------------------------------------------------- 1 | const functions = require('./functions'); 2 | for(var item in functions) global[item] = functions[item]; 3 | const { parentPort, workerData } = require('worker_threads'); 4 | 5 | const markdown = require('./namumark_parser'); 6 | 7 | markdown(workerData.req, workerData.content, workerData.discussion, workerData.title, workerData.flags, workerData.root) 8 | .then(data => parentPort.postMessage(data)) 9 | .catch(e => { 10 | log('렌더러', '오류! ' + e.stack); 11 | parentPort.postMessage(''); 12 | }); 13 | -------------------------------------------------------------------------------- /routes/reset_xref.js: -------------------------------------------------------------------------------- 1 | // 역링크 초기화 (디버그 전용) 2 | if(hostconfig.debug) router.get('/ResetXref', function(req, res) { 3 | print('기존 역링크 데이타 삭제'); 4 | curs.execute("delete from backlink") 5 | .then(() => { 6 | print('문서 목록 불러오기'); 7 | curs.execute("select title, namespace, content from documents") 8 | .then(async dbdocs => { 9 | print('초기화 시작...'); 10 | for(var item of dbdocs) { 11 | prt(totitle(item.title, item.namespace) + ' 처리 중... '); 12 | await markdown(req, item.content, 0, totitle(item.title, item.namespace) + '', 'backlinkinit'); 13 | print('완료!'); 14 | } 15 | print('모두 처리 완료.'); 16 | return res.send('완료!'); 17 | }); 18 | }); 19 | }); -------------------------------------------------------------------------------- /hostconfig.js: -------------------------------------------------------------------------------- 1 | const hostconfig = require('./config.json'); 2 | 3 | if(hostconfig.theseed_version === undefined) 4 | hostconfig.theseed_version = process.env.THESEED_VERSION || '4.12.0'; 5 | 6 | if(hostconfig.host === undefined) 7 | hostconfig.host = process.env.HOST || '0.0.0.0'; 8 | 9 | if(hostconfig.port === undefined) 10 | hostconfig.port = process.env.PORT || '8000'; 11 | 12 | if(hostconfig.disable_file_server === undefined) 13 | hostconfig.disable_file_server = !!Number(process.env.DISABLE_FILE_SERVER); 14 | 15 | if(hostconfig.database_type === undefined) 16 | hostconfig.database_type = (process.env.DATABASE_TYPE || '').toLowerCase() || 'sqlite'; 17 | 18 | module.exports = hostconfig; 19 | -------------------------------------------------------------------------------- /database.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const conn = new sqlite3.Database('./wikidata.db', () => 0); // 데이타베이스 3 | 4 | // 파이선 SQLite 모방 5 | const curs = { 6 | execute(sql, params = []) { 7 | return new Promise((resolve, reject) => { 8 | if(sql.toUpperCase().startsWith("SELECT")) { 9 | conn.all(sql, params, (err, retval) => { 10 | if(err) return reject(err); 11 | resolve(retval); 12 | }); 13 | } else { 14 | conn.run(sql, params, err => { 15 | if(err) return reject(err); 16 | resolve(0); 17 | }); 18 | } 19 | }); 20 | } 21 | }; 22 | 23 | // 데이타 베이스에 추가 24 | function insert(table, obj) { 25 | var arr = []; 26 | var sql = 'insert into ' + table + ' ('; 27 | for(var item in obj) { 28 | sql += item + ', '; 29 | } sql = sql.replace(/[,]\s$/, '') + ') values ('; 30 | for(var item in obj) { 31 | sql += '?, '; 32 | arr.push(obj[item]); 33 | } sql = sql.replace(/[,]\s$/, '') + ')'; 34 | return curs.execute(sql, arr); 35 | } 36 | 37 | module.exports = { 38 | conn, curs, insert, 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /routes/blame.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/blame\/(.*)/, async (req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | const rev = req.query['rev']; 5 | 6 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 7 | if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg })); 8 | if(!rev) { 9 | var d = await curs.execute("select rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 10 | if(d.length) rev = d[0].rev; 11 | else return res.send(await showError(req, 'revision_not_found')); 12 | } 13 | var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 14 | if(!dbdata.length) return res.send(await showError(req, 'revision_not_found')); 15 | const revdata = dbdata[0]; 16 | 17 | var content = ` 18 | 미구현 19 | `; 20 | 21 | res.send(await render(req, doc + ' (Blame)', content, { 22 | rev, 23 | document: doc, 24 | }, _, null, 'blame')); 25 | }); -------------------------------------------------------------------------------- /backend/unfinished/blame.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/blame\/(.*)/, async (req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | const rev = req.query['rev']; 5 | 6 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 7 | if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg })); 8 | if(!rev) { 9 | var d = await curs.execute("select rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 10 | if(d.length) rev = d[0].rev; 11 | else return res.send(await showError(req, 'revision_not_found')); 12 | } 13 | var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 14 | if(!dbdata.length) return res.send(await showError(req, 'revision_not_found')); 15 | const revdata = dbdata[0]; 16 | 17 | var content = ` 18 | 미구현 19 | `; 20 | 21 | res.send(await render(req, doc + ' (Blame)', content, { 22 | rev, 23 | document: doc, 24 | }, _, null, 'blame')); 25 | }); -------------------------------------------------------------------------------- /frontends/nuxt/frontend.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const geoip = require('geoip-lite'); 3 | const inputReader = require('wait-console-input'); 4 | const { SHA3 } = require('sha3'); 5 | const md5 = require('md5'); 6 | const session = require('express-session'); 7 | const swig = require('swig'); 8 | const ipRangeCheck = require('ip-range-check'); 9 | const bodyParser = require('body-parser'); 10 | const fs = require('fs'); 11 | const { JSDOM } = require('jsdom'); 12 | const jquery = require('jquery'); 13 | const diff = require('../../cemerick-jsdifflib.js'); 14 | const cookieParser = require('cookie-parser'); 15 | const child_process = require('child_process'); 16 | const captchapng = require('captchapng'); 17 | 18 | const express = require('express'); 19 | const router = express.Router(); 20 | const hostconfig = require('../../hostconfig'); 21 | const functions = require('../../functions'); 22 | const markdown = require('../../namumark'); 23 | for(var item in functions) global[item] = functions[item]; 24 | const API = require('../../backend/backend'); 25 | 26 | router.get(/^\/internal\/w\/(.*)/, async function viewDocument(req, res) { 27 | return res.json(await API.viewDocument(req, req.params[0])); 28 | }); 29 | 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /namumark.js: -------------------------------------------------------------------------------- 1 | const hostconfig = require('./hostconfig'); 2 | const os = require('os'); 3 | const functions = require('./functions'); 4 | for(var item in functions) global[item] = functions[item]; 5 | 6 | var available = true; 7 | try { 8 | if(!hostconfig.force_enable_multithreading) throw 1; 9 | if(os.cpus().length < 2) throw 1; 10 | if(process.versions.node.split('.')[0] < 16) throw 1; 11 | if(hostconfig.disable_multithreading && !hostconfig.force_enable_multithreading) throw 1; 12 | require('worker_threads'); 13 | } catch(e) { 14 | available = false; 15 | console.warn('[알림!]: 멀티 쓰레딩이 꺼져 있습니다'); 16 | } 17 | 18 | if(available) { 19 | const { Worker } = require('worker_threads'); 20 | module.exports = function markdown(req, content, discussion = 0, title = '', flags = '', root = '') { 21 | return new Promise((resolve, reject) => { 22 | const worker = new Worker('./namumark_parser_multithreaded.js', { 23 | workerData: { req: simplifyRequest(req), content, discussion, title, flags, root } 24 | }); 25 | 26 | worker.on('error', e => { 27 | throw e; 28 | reject(e); 29 | }); 30 | 31 | worker.on('message', ret => { 32 | resolve(ret); 33 | }); 34 | }); 35 | }; 36 | } else { 37 | module.exports = require('./namumark_parser'); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /routes/diff.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/diff\/(.*)/, async (req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | const rev = req.query['rev']; 5 | const oldrev = req.query['oldrev']; 6 | 7 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 8 | if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg })); 9 | if(!rev || !oldrev || Number(rev) <= Number(oldrev)) return res.send(await showError(req, 'revision_not_found')); 10 | var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 11 | if(!dbdata.length) return res.send(await showError(req, 'revision_not_found')); 12 | const revdata = dbdata[0]; 13 | var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, oldrev]); 14 | if(!dbdata.length) return res.send(await showError(req, 'revision_not_found')); 15 | const oldrevdata = dbdata[0]; 16 | const diffoutput = diff(oldrevdata.content, revdata.content, 'r' + oldrev, 'r' + rev); 17 | var content = diffoutput; 18 | 19 | res.send(await render(req, doc + ' (비교)', content, { 20 | rev, 21 | oldrev, 22 | diffoutput, 23 | document: doc, 24 | }, _, null, 'diff')); 25 | }); -------------------------------------------------------------------------------- /routes/router.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const geoip = require('geoip-lite'); 3 | const inputReader = require('wait-console-input'); 4 | const { SHA3 } = require('sha3'); 5 | const md5 = require('md5'); 6 | const session = require('express-session'); 7 | const swig = require('swig'); 8 | const ipRangeCheck = require('ip-range-check'); 9 | const bodyParser = require('body-parser'); 10 | const fs = require('fs'); 11 | const diff = require('../cemerick-jsdifflib.js'); 12 | const cookieParser = require('cookie-parser'); 13 | const child_process = require('child_process'); 14 | const captchapng = require('captchapng'); 15 | const nodemailer = require('nodemailer'); 16 | 17 | const express = require('express'); 18 | const router = express.Router(); 19 | const hostconfig = require('../hostconfig'); 20 | const functions = require('../functions'); 21 | const markdown = require('../namumark'); 22 | const http = require('http'); 23 | for(var item in functions) global[item] = functions[item]; 24 | 25 | for(var src of fs.readdirSync('./routes', { withFileTypes: true }).filter(f => !(fs.statSync('./routes/' + (f.name || f)).isDirectory())).map(dirent => dirent.name || dirent)) { 26 | if(src.toLowerCase() == 'router.js') continue; 27 | eval(fs.readFileSync('./routes/' + src).toString()); 28 | } 29 | 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /backend/unfinished/diff.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/diff\/(.*)/, async (req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | const rev = req.query['rev']; 5 | const oldrev = req.query['oldrev']; 6 | 7 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 8 | if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg })); 9 | if(!rev || !oldrev || Number(rev) <= Number(oldrev)) return res.send(await showError(req, 'revision_not_found')); 10 | var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 11 | if(!dbdata.length) return res.send(await showError(req, 'revision_not_found')); 12 | const revdata = dbdata[0]; 13 | var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, oldrev]); 14 | if(!dbdata.length) return res.send(await showError(req, 'revision_not_found')); 15 | const oldrevdata = dbdata[0]; 16 | const diffoutput = diff(oldrevdata.content, revdata.content, 'r' + oldrev, 'r' + rev); 17 | var content = diffoutput; 18 | 19 | res.send(await render(req, doc + ' (비교)', content, { 20 | rev, 21 | oldrev, 22 | diffoutput, 23 | document: doc, 24 | }, _, null, 'diff')); 25 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": "12" 4 | }, 5 | "name": "imitated-seed-v2", 6 | "version": "4.12.0", 7 | "description": "더 시드 모방 엔진.", 8 | "main": "server.js", 9 | "dependencies": { 10 | "body-parser": "^1.19.0", 11 | "captchapng": "0.0.1", 12 | "cookie-parser": "^1.4.5", 13 | "express": "^4.17.1", 14 | "express-fileupload": "^1.1.10", 15 | "express-session": "^1.17.2", 16 | "geoip-lite": "^1.4.5", 17 | "image-size": "^0.5.5", 18 | "ip-range-check": "^0.2.0", 19 | "js-sha256": "^0.11.0", 20 | "jsdom": "^11.12.0", 21 | "md5": "^2.3.0", 22 | "nodemailer": "6.9.1", 23 | "request": "^2.88.2", 24 | "sha3": "^2.1.4", 25 | "sqlite3": "^4.2.0", 26 | "stream-json": "^1.8.0", 27 | "swig": "^1.4.2", 28 | "wait-console-input": "^0.1.7" 29 | }, 30 | "scripts": { 31 | "test": "echo \"Error: no test specified\" && exit 1", 32 | "start": "node server" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/gdl-888/imitated-seed-v2.git" 37 | }, 38 | "keywords": [ 39 | "theseed", 40 | "the-seed", 41 | "node.js" 42 | ], 43 | "author": "ㅇ", 44 | "license": "UNLICENSED", 45 | "bugs": { 46 | "url": "https://github.com/gdl-888/imitated-seed-v2/issues" 47 | }, 48 | "homepage": "https://github.com/gdl-888/imitated-seed-v2#readme" 49 | } 50 | -------------------------------------------------------------------------------- /fileserver.js: -------------------------------------------------------------------------------- 1 | // 설정 변수 변경 2 | const maxFileSize = 2000000; // 최대 화일 크기 (기본값 2MB) 3 | const host = '127.5.5.5'; // 호스트 주소 4 | const port = 27775; // 포트 5 | 6 | const http = require('http'); 7 | const path = require('path'); 8 | const express = require('express'); 9 | const bodyParser = require('body-parser'); 10 | const fs = require('fs'); 11 | const fileUpload = require('express-fileupload'); 12 | 13 | const { sha256 } = require('js-sha256'); 14 | const sizeOf = require('image-size'); 15 | 16 | function print(x) { console.log(x); } 17 | function prt(x) { process.stdout.write(x); } 18 | 19 | const server = express(); 20 | 21 | server.use(bodyParser.json()); 22 | server.use(bodyParser.urlencoded({ extended: true })); 23 | server.use(fileUpload({ 24 | limits: { fileSize: maxFileSize }, 25 | abortOnLimit: true, 26 | })); 27 | 28 | server.disable('x-powered-by'); 29 | 30 | server.post('/upload', function(req, res) { 31 | if(!req.files || !req.files.file) 32 | return res.status(400).send(''); 33 | const file = req.files.file; 34 | const hash = sha256(file.data); 35 | file.mv(`./images/${hash.slice(0, 2)}/${hash}`, err => { 36 | if(err) 37 | return res.json({ status: 'error' }); 38 | var w = 0, h = 0; 39 | sizeOf(`./images/${hash.slice(0, 2)}/${hash}`, function (err, dimensions) { 40 | if(!err) w = dimensions.width, h = dimensions.height; 41 | return res.json({ status: 'success', name: file.name, hash, size: file.data.length, width: w, height: h }); 42 | }); 43 | }); 44 | }); 45 | 46 | server.use('/', express.static('images')); 47 | 48 | server.listen(port, host); 49 | print(host + (port == 80 ? '' : (':' + port)) + '에서 실행 중. . .'); 50 | -------------------------------------------------------------------------------- /routes/raw.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/raw\/(.*)/, async(req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | var rev = req.query['rev']; 5 | 6 | if(title.replace(/\s/g, '') === '') { 7 | return res.send(await await showError(req, 'invalid_title')); 8 | } 9 | 10 | if(rev) { 11 | var data = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 12 | } else { 13 | var data = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 14 | } 15 | const rawContent = data; 16 | if(!rev) { 17 | var data = await curs.execute("select rev from history where title = ? and namespace = ? order by CAST(rev AS INTEGER) desc limit 1", [doc.title, doc.namespace]); 18 | if(data.length) rev = data[0].rev; 19 | } 20 | var content = ''; 21 | 22 | try { 23 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 24 | if(aclmsg) { 25 | return res.send(await await showError(req, { code: 'permission_read', msg: aclmsg })); 26 | } else { 27 | content = rawContent[0].content; 28 | } 29 | } catch(e) { 30 | return res.status(404).send(await showError(req, 'document_not_found')); 31 | } 32 | 33 | if(!ver('4.16.0')) { 34 | res.setHeader('Content-Type', 'text/plain'); 35 | return res.send(content); 36 | } 37 | 38 | var rtcontent = ` 39 | 40 | `; 41 | 42 | res.send(await render(req, totitle(doc.title, doc.namespace) + ' (r' + rev + ' RAW)', rtcontent, { 43 | document: doc, 44 | rev, 45 | }, '', null, 'raw')); 46 | }); -------------------------------------------------------------------------------- /backend/unfinished/raw.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/raw\/(.*)/, async(req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | var rev = req.query['rev']; 5 | 6 | if(title.replace(/\s/g, '') === '') { 7 | return res.send(await await showError(req, 'invalid_title')); 8 | } 9 | 10 | if(rev) { 11 | var data = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 12 | } else { 13 | var data = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 14 | } 15 | const rawContent = data; 16 | if(!rev) { 17 | var data = await curs.execute("select rev from history where title = ? and namespace = ? order by CAST(rev AS INTEGER) desc limit 1", [doc.title, doc.namespace]); 18 | if(data.length) rev = data[0].rev; 19 | } 20 | var content = ''; 21 | 22 | try { 23 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 24 | if(aclmsg) { 25 | return res.send(await await showError(req, { code: 'permission_read', msg: aclmsg })); 26 | } else { 27 | content = rawContent[0].content; 28 | } 29 | } catch(e) { 30 | return res.status(404).send(await showError(req, 'document_not_found')); 31 | } 32 | 33 | if(!ver('4.16.0')) { 34 | res.setHeader('Content-Type', 'text/plain'); 35 | return res.send(content); 36 | } 37 | 38 | var rtcontent = ` 39 | 40 | `; 41 | 42 | res.send(await render(req, totitle(doc.title, doc.namespace) + ' (r' + rev + ' RAW)', rtcontent, { 43 | document: doc, 44 | rev, 45 | }, '', null, 'raw')); 46 | }); -------------------------------------------------------------------------------- /routes/randompage.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/RandomPage$/, async function randomPage(req, res) { 2 | const usens = ver('4.5.5'); 3 | 4 | const nslist = fetchNamespaces(); 5 | var ns = usens ? req.query['namespace'] : null; 6 | if(!ns || !nslist.includes(ns)) ns = '문서'; 7 | 8 | var content = ''; 9 | 10 | if(usens) { 11 | content = ` 12 |
13 |
14 |
15 | 16 | 26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | `; 34 | } 35 | 36 | content += ` 37 | '; 52 | 53 | res.send(await render(req, 'RandomPage', content, {})); 54 | }); -------------------------------------------------------------------------------- /backend/unfinished/randompage.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/RandomPage$/, async function randomPage(req, res) { 2 | const usens = ver('4.5.5'); 3 | 4 | const nslist = fetchNamespaces(); 5 | var ns = usens ? req.query['namespace'] : null; 6 | if(!ns || !nslist.includes(ns)) ns = '문서'; 7 | 8 | var content = ''; 9 | 10 | if(usens) { 11 | content = ` 12 |
13 |
14 |
15 | 16 | 26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | `; 34 | } 35 | 36 | content += ` 37 | '; 52 | 53 | res.send(await render(req, 'RandomPage', content, {})); 54 | }); -------------------------------------------------------------------------------- /routes/contribution_discuss.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/contribution\/(ip|author)\/(.+)\/discuss$/, async function discussionLog(req, res) { 2 | const ismember = req.params[0]; 3 | const username = req.params[1]; 4 | 5 | var dd = await curs.execute("select id, tnum, time, username, ismember from res \ 6 | where cast(time as integer) >= ? and ismember = ? and lower(username) = ? order by cast(time as integer) desc", [ 7 | Number(getTime()) - 2592000000, ismember, username.toLowerCase() 8 | ]); 9 | 10 | var content = ` 11 |

최근 30일동안의 기여 목록 입니다.

12 | 13 | 17 | 18 | 19 | 20 | 21 | ${ver('4.13.0') ? '' : ``} 22 | 23 | 24 | 25 | 26 | 27 | 28 | ${ver('4.13.0') ? '' : ``} 29 | 30 | 31 | 32 | 33 | 34 | `; 35 | 36 | for(var row of dd) { 37 | const td = (await curs.execute("select title, namespace, topic from threads where tnum = ?", [row.tnum]))[0]; 38 | const title = totitle(td.title, td.namespace) + ''; 39 | 40 | content += ` 41 | 42 | 45 | 46 | ${ver('4.13.0') ? '' : ` 47 | 50 | `} 51 | 52 | 55 | 56 | `; 57 | } 58 | content += ` 59 | 60 |
항목수정자수정 시간
43 | #${row.id} ${html.escape(td['topic'])} (${html.escape(title)}) 44 | 48 | ${ip_pas(row.username, row.ismember)} 49 | 53 | ${generateTime(toDate(row.time), timeFormat)} 54 |
61 | `; 62 | 63 | res.send(await render(req, `"${username}" 기여 목록`, content, {})); 64 | }); -------------------------------------------------------------------------------- /backend/unfinished/contribution_discuss.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/contribution\/(ip|author)\/(.+)\/discuss$/, async function discussionLog(req, res) { 2 | const ismember = req.params[0]; 3 | const username = req.params[1]; 4 | 5 | var dd = await curs.execute("select id, tnum, time, username, ismember from res \ 6 | where cast(time as integer) >= ? and ismember = ? and lower(username) = ? order by cast(time as integer) desc", [ 7 | Number(getTime()) - 2592000000, ismember, username.toLowerCase() 8 | ]); 9 | 10 | var content = ` 11 |

최근 30일동안의 기여 목록 입니다.

12 | 13 | 17 | 18 | 19 | 20 | 21 | ${ver('4.13.0') ? '' : ``} 22 | 23 | 24 | 25 | 26 | 27 | 28 | ${ver('4.13.0') ? '' : ``} 29 | 30 | 31 | 32 | 33 | 34 | `; 35 | 36 | for(var row of dd) { 37 | const td = (await curs.execute("select title, namespace, topic from threads where tnum = ?", [row.tnum]))[0]; 38 | const title = totitle(td.title, td.namespace) + ''; 39 | 40 | content += ` 41 | 42 | 45 | 46 | ${ver('4.13.0') ? '' : ` 47 | 50 | `} 51 | 52 | 55 | 56 | `; 57 | } 58 | content += ` 59 | 60 |
항목수정자수정 시간
43 | #${row.id} ${html.escape(td['topic'])} (${html.escape(title)}) 44 | 48 | ${ip_pas(row.username, row.ismember)} 49 | 53 | ${generateTime(toDate(row.time), timeFormat)} 54 |
61 | `; 62 | 63 | res.send(await render(req, `"${username}" 기여 목록`, content, {})); 64 | }); -------------------------------------------------------------------------------- /undelete-thread.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const inputReader = require('wait-console-input'); 3 | const conn = new sqlite3.Database('./wikidata.db', () => 1); 4 | 5 | function Split(str, del) { return str.split(del); }; const split = Split; 6 | function UCase(s) { return s.toUpperCase(); }; const ucase = UCase; 7 | function LCase(s) { return s.toUpperCase(); }; const lcase = LCase; 8 | 9 | function print(x) { console.log(x); } 10 | function prt(x) { process.stdout.write(x); } 11 | function input(prpt) { 12 | prt(prpt); 13 | return inputReader.readLine(''); 14 | } 15 | 16 | conn.commit = function() {}; 17 | conn.sd = []; 18 | 19 | const curs = { 20 | execute: function executeSQL(sql = '', params = []) { 21 | return new Promise((resolve, reject) => { 22 | if(UCase(sql).startsWith("SELECT")) { 23 | conn.all(sql, params, (err, retval) => { 24 | if(err) return reject(err); 25 | conn.sd = retval; 26 | resolve(retval); 27 | }); 28 | } else { 29 | conn.run(sql, params, err => { 30 | if(err) return reject(err); 31 | resolve(0); 32 | }); 33 | } 34 | }); 35 | }, 36 | fetchall: function fetchSQLData() { 37 | return conn.sd; 38 | }, 39 | }; 40 | 41 | curs.execute("select title, namespace, topic, tnum from threads where deleted = '1'") 42 | .then(d => { 43 | var num = 1; 44 | for(item of d) { 45 | with(item) 46 | print(`[${num++}] ${namespace != '문서' ? (namespace + ':' + title) : title} - ${topic} (${tnum})`); 47 | } 48 | var sel = input('토론 번호 또는 좌표: '); 49 | var seln = Number(sel); 50 | if(!seln) { 51 | curs.execute("update threads set deleted = '0' where tnum = ?", [sel]) 52 | .then(() => { 53 | print('복구됨.'); 54 | }) 55 | .catch(() => { 56 | print('복구할 수 없습니다'); 57 | }); 58 | } else if(!d[seln-1]) { 59 | print('복구할 수 없습니다'); 60 | } else { 61 | curs.execute("update threads set deleted = '0' where tnum = ?", [d[seln-1].tnum]) 62 | .then(() => { 63 | print('복구됨.'); 64 | }); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /routes/sidebar.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/sidebar[.]json$/, (req, res) => { 2 | curs.execute("select time, title, namespace from history where namespace = '문서' order by cast(time as integer) desc limit 1000") 3 | .then(async dbdata => { 4 | var ret = [], cnt = 0, used = []; 5 | for(var item of dbdata) { 6 | if(used.includes(item.title)) continue; 7 | used.push(item.title); 8 | 9 | const del = (await curs.execute("select title from documents where title = ? and namespace = '문서'", [item.title])).length; 10 | ret.push({ 11 | document: totitle(item.title, '문서') + '', 12 | status: (del ? 'normal' : 'delete'), 13 | date: Math.floor(Number(item.time) / 1000), 14 | }); 15 | cnt++; 16 | if(cnt > 20) break; 17 | } 18 | res.json(ret); 19 | }) 20 | .catch(e => { 21 | print(e.stack); 22 | res.json('[]'); 23 | }); 24 | }); 25 | 26 | router.get(/^\/api\/sidebar$/, async(req, res) => { 27 | var cret = [], dret = [], cnt, used; 28 | var dbdata = await curs.execute("select time, title, namespace from history order by cast(time as integer) desc limit 1000"); 29 | cnt = 0, used = [] 30 | for(var item of dbdata) { 31 | if(used.includes(item.title)) continue; 32 | used.push(item.title); 33 | const del = (await curs.execute("select title from documents where title = ? and namespace = ?", [item.title, item.namespace])).length; 34 | cret.push({ 35 | document: totitle(item.title, item.namespace) + '', 36 | status: (del ? 'normal' : 'delete'), 37 | date: Math.floor(Number(item.time) / 1000), 38 | }); 39 | cnt++; 40 | if(cnt > 10) break; 41 | } 42 | var dbdata = await curs.execute("select time, num, topic, title, namespace from threads order by cast(time as integer) desc limit 1000"); 43 | cnt = 0, used = [] 44 | for(var item of dbdata) { 45 | if(used.includes(item.num)) continue; 46 | used.push(item.num); 47 | dret.push({ 48 | document: totitle(item.title, item.namespace) + '', 49 | topic: item.topic, 50 | date: Math.floor(Number(item.time) / 1000), 51 | id: Number(item.num), 52 | }); 53 | cnt++; 54 | if(cnt > 10) break; 55 | } 56 | res.json({ 57 | document: cret, 58 | discuss: dret, 59 | }); 60 | }); -------------------------------------------------------------------------------- /routes/star.js: -------------------------------------------------------------------------------- 1 | if(ver('4.9.0')) router.get(/^\/member\/star\/(.*)$/, async (req, res) => { 2 | const title = req.params[0]; 3 | if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/star/' + title)); 4 | const doc = processTitle(title); 5 | 6 | var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]); 7 | if(dbdata.length) return res.send(await showError(req, 'already_starred_document')); 8 | 9 | var dbdata = await curs.execute("select time from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 10 | if(!dbdata.length) return res.send(await showError(req, 'document_not_found')); 11 | 12 | await curs.execute('insert into stars (title, namespace, username, lastedit) values (?, ?, ?, ?)', [doc.title, doc.namespace, ip_check(req), dbdata[0]['time']]); 13 | 14 | res.redirect('/w/' + encodeURIComponent(title)); 15 | }); 16 | 17 | if(ver('4.9.0')) router.get(/^\/member\/unstar\/(.*)$/, async (req, res) => { 18 | const title = req.params[0]; 19 | if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/star/' + title)); 20 | const doc = processTitle(title); 21 | 22 | var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]); 23 | if(!dbdata.length) return res.send(await showError(req, 'already_unstarred_document')); 24 | 25 | var dbdata = await curs.execute("select time from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 26 | if(!dbdata.length) return res.send(await showError(req, 'document_not_found')); 27 | 28 | 29 | await curs.execute('delete from stars where title = ? and namespace = ? and username = ?', [doc.title, doc.namespace, ip_check(req)]); 30 | 31 | res.redirect('/w/' + encodeURIComponent(title)); 32 | }); 33 | 34 | 35 | if(ver('4.9.0')) router.get(/^\/member\/starred_documents$/, async (req, res) => { 36 | if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/starred_documents')); 37 | 38 | var dd = await curs.execute("select title, namespace, lastedit from stars where username = ? order by cast(lastedit as integer) desc", [ip_check(req)]); 39 | var content = `'; 49 | 50 | res.send(await render(req, '내 문서함', content, {}, _, _, 'starred_documents')); 51 | }); -------------------------------------------------------------------------------- /backend/unfinished/star.js: -------------------------------------------------------------------------------- 1 | if(ver('4.9.0')) router.get(/^\/member\/star\/(.*)$/, async (req, res) => { 2 | const title = req.params[0]; 3 | if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/star/' + title)); 4 | const doc = processTitle(title); 5 | 6 | var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]); 7 | if(dbdata.length) return res.send(await showError(req, 'already_starred_document')); 8 | 9 | var dbdata = await curs.execute("select time from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 10 | if(!dbdata.length) return res.send(await showError(req, 'document_not_found')); 11 | 12 | await curs.execute('insert into stars (title, namespace, username, lastedit) values (?, ?, ?, ?)', [doc.title, doc.namespace, ip_check(req), dbdata[0]['time']]); 13 | 14 | res.redirect('/w/' + encodeURIComponent(title)); 15 | }); 16 | 17 | if(ver('4.9.0')) router.get(/^\/member\/unstar\/(.*)$/, async (req, res) => { 18 | const title = req.params[0]; 19 | if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/star/' + title)); 20 | const doc = processTitle(title); 21 | 22 | var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]); 23 | if(!dbdata.length) return res.send(await showError(req, 'already_unstarred_document')); 24 | 25 | var dbdata = await curs.execute("select time from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 26 | if(!dbdata.length) return res.send(await showError(req, 'document_not_found')); 27 | 28 | 29 | await curs.execute('delete from stars where title = ? and namespace = ? and username = ?', [doc.title, doc.namespace, ip_check(req)]); 30 | 31 | res.redirect('/w/' + encodeURIComponent(title)); 32 | }); 33 | 34 | 35 | if(ver('4.9.0')) router.get(/^\/member\/starred_documents$/, async (req, res) => { 36 | if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/starred_documents')); 37 | 38 | var dd = await curs.execute("select title, namespace, lastedit from stars where username = ? order by cast(lastedit as integer) desc", [ip_check(req)]); 39 | var content = `'; 49 | 50 | res.send(await render(req, '내 문서함', content, {}, _, _, 'starred_documents')); 51 | }); -------------------------------------------------------------------------------- /backlink-reset.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const conn = new sqlite3.Database('./wikidata.db', () => 1); 3 | 4 | function Split(str, del) { return str.split(del); }; const split = Split; 5 | function UCase(s) { return s.toUpperCase(); }; const ucase = UCase; 6 | function LCase(s) { return s.toUpperCase(); }; const lcase = LCase; 7 | 8 | function print(x) { console.log(x); } 9 | function prt(x) { process.stdout.write(x); } 10 | 11 | conn.commit = function() {}; 12 | conn.sd = []; 13 | 14 | const curs = { 15 | execute: function executeSQL(sql = '', params = []) { 16 | return new Promise((resolve, reject) => { 17 | if(UCase(sql).startsWith("SELECT")) { 18 | conn.all(sql, params, (err, retval) => { 19 | if(err) return reject(err); 20 | conn.sd = retval; 21 | resolve(retval); 22 | }); 23 | } else { 24 | conn.run(sql, params, err => { 25 | if(err) return reject(err); 26 | resolve(0); 27 | }); 28 | } 29 | }); 30 | }, 31 | fetchall: function fetchSQLData() { 32 | return conn.sd; 33 | }, 34 | }; 35 | 36 | function totitle(t, ns) { 37 | const nslist = fetchNamespaces(); 38 | var forceShowNamespace = false; 39 | if(ns == '문서' && nslist.includes(t.split(':')[0]) && t.split(':')[1] !== undefined) 40 | forceShowNamespace = true; 41 | 42 | return { 43 | title: t, 44 | namespace: ns, 45 | forceShowNamespace, 46 | toString() { 47 | if(forceShowNamespace || this.namespace != '문서') 48 | return this.namespace + ':' + this.title; 49 | else 50 | return this.title; 51 | } 52 | }; 53 | } 54 | 55 | const wikiconfig = {}; 56 | const hostconfig = require('./config.json'); 57 | 58 | const config = { 59 | getString(str, def = '') { 60 | if(typeof(wikiconfig[str]) == 'undefined') { 61 | curs.execute("insert into config (key, value) values (?, ?)", [str, def]); 62 | wikiconfig[str] = def; 63 | return def; 64 | } 65 | return wikiconfig[str]; 66 | } 67 | }; 68 | 69 | function fetchNamespaces() { 70 | return ['문서', '틀', '분류', '파일', '사용자', '특수기능', config.getString('wiki.site_name', '더 시드'), '토론', '휴지통', '투표'].concat(hostconfig.custom_namespaces || []); 71 | } 72 | 73 | (async() => { 74 | var data = await curs.execute("select key, value from config"); 75 | for(var cfg of data) { 76 | wikiconfig[cfg.key] = cfg.value; 77 | } 78 | 79 | print('기존 역링크 데이타 삭제 중...'); 80 | curs.execute("delete from backlink") 81 | .then(() => { 82 | print('문서 목록을 불러오는 중...'); 83 | curs.execute("select title, namespace, content from documents") 84 | .then(async dbdocs => { 85 | print('초기화 시작...'); 86 | for(var item of dbdocs) { 87 | prt(totitle(item.title, item.namespace) + ' 처리 중... '); 88 | await markdown(item.content, 0, totitle(item.title, item.namespace) + '', 'backlinkinit'); 89 | print('완료!'); 90 | } 91 | print('모두 처리 완료.'); 92 | }); 93 | }); 94 | })(); 95 | 96 | -------------------------------------------------------------------------------- /backend/unfinished/recent_changes.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/RecentChanges$/, async function recentChanges(req, res) { 2 | var flag = req.query['logtype']; 3 | if(!['all', 'create', 'delete', 'move', 'revert'].includes(flag)) flag = 'all'; 4 | if(flag == 'all') flag = '%'; 5 | 6 | var data = await curs.execute("select isapi, flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \ 7 | where " + (flag == '%' ? "not namespace = '사용자' and " : '') + "advance like ? order by cast(time as integer) desc limit 100", 8 | [flag]); 9 | 10 | 11 | var content = ` 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | `; 37 | 38 | for(var row of data) { 39 | var title = totitle(row.title, row.namespace) + ''; 40 | 41 | content += ` 42 | 0 || row.advance != 'normal' ? ' class=no-line' : '')}> 43 | 66 | 67 | 70 | 71 | 74 | 75 | `; 76 | 77 | if(row.log.length > 0 || row.advance != 'normal') { 78 | content += ` 79 | 82 | `; 83 | } 84 | } 85 | 86 | content += ` 87 | 88 |
항목수정자수정 시간
44 | ${html.escape(title)} 45 | [역사] 46 | ${ 47 | Number(row.rev) > 1 48 | ? '[비교]' 49 | : '' 50 | } 51 | [토론] 52 | 53 | (${row.changes}) 65 | 68 | ${ip_pas(row.username, row.ismember)}${ver('4.20.0') && row.isapi ?' (API)' : ''} 69 | 72 | ${generateTime(toDate(row.time), timeFormat)} 73 |
80 | ${row.log} ${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : ''} 81 |
89 | `; 90 | 91 | res.send(await render(req, '최근 변경내역', content, {})); 92 | }); -------------------------------------------------------------------------------- /backend/unfinished/delete.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/delete\/(.*)/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | 4 | const title = req.params[0]; 5 | const doc = processTitle(title); 6 | 7 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2); 8 | if(aclmsg) return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 9 | 10 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'delete', 1); 11 | if(aclmsg) return res.send(await showError(req, { code: 'permission_delete', msg: aclmsg })); 12 | 13 | const o_o = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 14 | if(!o_o.length) return res.send(await showError(req, 'document_not_found')); 15 | 16 | var content = ` 17 |
18 |
19 | 20 | 21 |
22 | 23 | 26 | 27 |

28 | 알림! : 문서의 제목을 변경하려는 경우 문서 이동 기능을 사용해주세요. 문서 이동 기능을 사용할 수 없는 경우 토론 기능이나 게시판을 통해 대행 요청을 해주세요. 29 |

30 | 31 |
32 | 33 | 34 |
35 |
36 | `; 37 | 38 | var error = null; 39 | if(req.method == 'POST') do { 40 | if(doc.namespace == '사용자') 41 | if((ver('4.11.0') && !doc.title.includes('/')) || !ver('4.11.0')) { 42 | content = (error = err('alert', 'disable_user_document')) + content; 43 | break; 44 | } 45 | 46 | if(!req.body['agree']) { 47 | content = (error = err('alert', 'validator_required', 'agree')) + content; 48 | break; 49 | } 50 | 51 | const _recentRev = await curs.execute("select content, rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 52 | const recentRev = _recentRev[0]; 53 | 54 | await curs.execute("delete from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 55 | const rawChanges = 0 - recentRev.content.length; 56 | curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance) \ 57 | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ 58 | doc.title, doc.namespace, '', String(Number(recentRev.rev) + 1), ip_check(req), getTime(), '' + (rawChanges), req.body['log'] || '', '0', '-1', islogin(req) ? 'author' : 'ip', 'delete' 59 | ]); 60 | curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]); 61 | return res.redirect('/w/' + encodeURIComponent(doc + '')); 62 | } while(0); 63 | 64 | res.send(await render(req, doc + ' (삭제)', content, { 65 | document: doc, 66 | }, '', null, 'delete')); 67 | }); -------------------------------------------------------------------------------- /routes/delete.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/delete\/(.*)/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | 4 | const title = req.params[0]; 5 | const doc = processTitle(title); 6 | 7 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2, 1); 8 | if(aclmsg) return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 9 | 10 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'delete', 1); 11 | if(aclmsg) return res.send(await showError(req, { code: 'permission_delete', msg: aclmsg })); 12 | 13 | const o_o = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 14 | if(!o_o.length) return res.send(await showError(req, 'document_not_found')); 15 | 16 | var content = ` 17 |
18 |
19 | 20 | 21 |
22 | 23 | 26 | 27 |

28 | 알림! : 문서의 제목을 변경하려는 경우 문서 이동 기능을 사용해주세요. 문서 이동 기능을 사용할 수 없는 경우 토론 기능이나 게시판을 통해 대행 요청을 해주세요. 29 |

30 | 31 |
32 | 33 | 34 |
35 |
36 | `; 37 | 38 | var error = null; 39 | if(req.method == 'POST') do { 40 | if(doc.namespace == '사용자') 41 | if((ver('4.11.0') && !doc.title.includes('/')) || !ver('4.11.0')) { 42 | content = (error = err('alert', 'disable_user_document')) + content; 43 | break; 44 | } 45 | 46 | if(!req.body['agree']) { 47 | content = (error = err('alert', 'validator_required', 'agree')) + content; 48 | break; 49 | } 50 | 51 | const _recentRev = await curs.execute("select content, rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 52 | const recentRev = _recentRev[0]; 53 | 54 | await curs.execute("delete from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 55 | const rawChanges = 0 - recentRev.content.length; 56 | curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance) \ 57 | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ 58 | doc.title, doc.namespace, '', String(Number(recentRev.rev) + 1), ip_check(req), getTime(), '' + (rawChanges), req.body['log'] || '', '0', '-1', islogin(req) ? 'author' : 'ip', 'delete' 59 | ]); 60 | curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]); 61 | return res.redirect('/w/' + encodeURIComponent(doc + '')); 62 | } while(0); 63 | 64 | res.send(await render(req, doc + ' (삭제)', content, { 65 | document: doc, 66 | }, '', error, 'delete')); 67 | }); 68 | -------------------------------------------------------------------------------- /search.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const express = require('express'); 3 | const server = express(); 4 | const conn = new sqlite3.Database('./wikidata.db', () => 1); 5 | function Split(str, del) { return str.split(del); }; const split = Split; 6 | function UCase(s) { return s.toUpperCase(); }; const ucase = UCase; 7 | function LCase(s) { return s.toUpperCase(); }; const lcase = LCase; 8 | function print(x) { console.log(x); } 9 | function prt(x) { process.stdout.write(x); } 10 | conn.commit = function() {}; 11 | conn.sd = []; 12 | const curs = { 13 | execute: function executeSQL(sql = '', params = []) { 14 | return new Promise((resolve, reject) => { 15 | if(UCase(sql).startsWith("SELECT")) { 16 | conn.all(sql, params, (err, retval) => { 17 | if(err) return reject(err); 18 | conn.sd = retval; 19 | resolve(retval); 20 | }); 21 | } else { 22 | conn.run(sql, params, err => { 23 | if(err) return reject(err); 24 | resolve(0); 25 | }); 26 | } 27 | }); 28 | }, 29 | fetchall: function fetchSQLData() { 30 | return conn.sd; 31 | }, 32 | }; 33 | const html = { 34 | escape(content = '') { 35 | content = content.replace(/[&]/gi, '&'); 36 | content = content.replace(/["]/gi, '"'); 37 | content = content.replace(/[<]/gi, '<'); 38 | content = content.replace(/[>]/gi, '>'); 39 | 40 | return content; 41 | } 42 | }; 43 | String.prototype.replaceAll = function(tofind, replacewith, matchcase = 1) { 44 | if(matchcase) { 45 | var esc = tofind.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 46 | var reg = new RegExp(esc, 'ig'); 47 | return this.replace(reg, replacewith); 48 | } else { 49 | var ss = this; 50 | while(ss.includes(tofind)) { 51 | ss = ss.replace(tofind, replacewith); 52 | } 53 | return ss; 54 | } 55 | }; 56 | 57 | const ranking = []; 58 | 59 | server.get(/^\/search\/(.*)/, async(req, res) => { 60 | const query = req.params[0]; 61 | const page = Number(req.query['page'] || '1'); 62 | var limit = 0; 63 | if(page * 10 > 0) limit = page * 10 - 10; 64 | var fdata = await curs.execute("select title, namespace, content from documents where (title like '%' || ? || '%' or content like '%' || ? || '%') order by title COLLATE NOCASE asc", [query, query]); 65 | var data = await curs.execute("select title, namespace, content from documents where (title like '%' || ? || '%' or content like '%' || ? || '%') order by title asc limit ?, 10 COLLATE NOCASE", [query, query, limit]); 66 | const ret = { page, lastpage: Math.ceil(fdata.length / 10), total: fdata.length, result: [] }; 67 | for(var item of data) { 68 | ret.result.push({ 69 | title: item.title, 70 | namespace: item.namespace, 71 | content: html.escape(item.content.slice(item.content.indexOf(query) - 250, query.length + 250)).replaceAll(html.escape(query), '' + html.escape(query) + ''), 72 | }); 73 | } 74 | res.json(ret); 75 | }); 76 | 77 | server.get(/^\/api\/ranking$/, (req, res) => { 78 | res.json(ranking.sort((l, r) => r.count - l.count).map(item => item.keyword).slice(0, 10)); 79 | }); 80 | 81 | server.listen(25005, '127.5.5.5', e => { 82 | print('실행 중.'); 83 | }); 84 | -------------------------------------------------------------------------------- /routes/revert.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/revert\/(.*)/, async (req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | const title = req.params[0]; 4 | const doc = processTitle(title); 5 | 6 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 7 | if(aclmsg) { 8 | return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg })); 9 | } 10 | 11 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2); 12 | if(aclmsg) { 13 | return res.status(403).send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 14 | } 15 | 16 | const rev = req.query['rev']; 17 | if(!rev || isNaN(Number(rev))) { 18 | return res.send(await showError(req, 'revision_not_found')); 19 | } 20 | 21 | const _recentRev = await curs.execute("select content, rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 22 | if(!_recentRev.length) { 23 | return res.send(await showError(req, 'document_not_found')); 24 | } 25 | 26 | const dbdata = await curs.execute("select content, advance, flags from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 27 | if(!dbdata.length) { 28 | return res.send(await showError(req, 'revision_not_found')); 29 | } 30 | const revdata = dbdata[0]; 31 | const recentRev = _recentRev[0]; 32 | 33 | // 더 시드에서 실제로는 되돌려짐. 34 | if(req.method == 'GET' && ['move', 'delete', 'acl', 'revert'].includes(revdata.advance)) { 35 | return res.send(await showError(req, 'not_revertable')); 36 | } 37 | 38 | var content = ` 39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 | 47 |
48 |
49 | `; 50 | 51 | if(req.method == 'POST') { 52 | if(recentRev.content == revdata.content) { 53 | return res.send(await showError(req, 'text_unchanged')); 54 | } 55 | await curs.execute("delete from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 56 | await curs.execute("insert into documents (content, title, namespace) values (?, ?, ?)", [revdata.content, doc.title, doc.namespace]); 57 | const rawChanges = revdata.content.length - recentRev.content.length; 58 | curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \ 59 | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ 60 | doc.title, doc.namespace, revdata.content, String(Number(recentRev.rev) + 1), ip_check(req), getTime(), (rawChanges > 0 ? '+' : '') + rawChanges, req.body['log'] || '', '0', '-1', islogin(req) ? 'author' : 'ip', 'revert', rev 61 | ]); 62 | curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]); 63 | return res.redirect('/w/' + encodeURIComponent(doc + '')); 64 | } 65 | 66 | res.send(await render(req, doc + ' (r' + rev + '로 되돌리기)', content, { 67 | rev, 68 | text: revdata.content, 69 | document: doc, 70 | }, _, null, 'revert')) 71 | }); -------------------------------------------------------------------------------- /backend/unfinished/revert.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/revert\/(.*)/, async (req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | const title = req.params[0]; 4 | const doc = processTitle(title); 5 | 6 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 7 | if(aclmsg) { 8 | return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg })); 9 | } 10 | 11 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2); 12 | if(aclmsg) { 13 | return res.status(403).send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 14 | } 15 | 16 | const rev = req.query['rev']; 17 | if(!rev || isNaN(Number(rev))) { 18 | return res.send(await showError(req, 'revision_not_found')); 19 | } 20 | 21 | const _recentRev = await curs.execute("select content, rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 22 | if(!_recentRev.length) { 23 | return res.send(await showError(req, 'document_not_found')); 24 | } 25 | 26 | const dbdata = await curs.execute("select content, advance, flags from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 27 | if(!dbdata.length) { 28 | return res.send(await showError(req, 'revision_not_found')); 29 | } 30 | const revdata = dbdata[0]; 31 | const recentRev = _recentRev[0]; 32 | 33 | // 더 시드에서 실제로는 되돌려짐. 34 | if(req.method == 'GET' && ['move', 'delete', 'acl', 'revert'].includes(revdata.advance)) { 35 | return res.send(await showError(req, 'not_revertable')); 36 | } 37 | 38 | var content = ` 39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 | 47 |
48 |
49 | `; 50 | 51 | if(req.method == 'POST') { 52 | if(recentRev.content == revdata.content) { 53 | return res.send(await showError(req, 'text_unchanged')); 54 | } 55 | await curs.execute("delete from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 56 | await curs.execute("insert into documents (content, title, namespace) values (?, ?, ?)", [revdata.content, doc.title, doc.namespace]); 57 | const rawChanges = revdata.content.length - recentRev.content.length; 58 | curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \ 59 | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ 60 | doc.title, doc.namespace, revdata.content, String(Number(recentRev.rev) + 1), ip_check(req), getTime(), (rawChanges > 0 ? '+' : '') + rawChanges, req.body['log'] || '', '0', '-1', islogin(req) ? 'author' : 'ip', 'revert', rev 61 | ]); 62 | curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]); 63 | return res.redirect('/w/' + encodeURIComponent(doc + '')); 64 | } 65 | 66 | res.send(await render(req, doc + ' (r' + rev + '로 되돌리기)', content, { 67 | rev, 68 | text: revdata.content, 69 | document: doc, 70 | }, _, null, 'revert')) 71 | }); -------------------------------------------------------------------------------- /backend/backend.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const geoip = require('geoip-lite'); 3 | const inputReader = require('wait-console-input'); 4 | const { SHA3 } = require('sha3'); 5 | const md5 = require('md5'); 6 | const session = require('express-session'); 7 | const swig = require('swig'); 8 | const ipRangeCheck = require('ip-range-check'); 9 | const bodyParser = require('body-parser'); 10 | const fs = require('fs'); 11 | const { JSDOM } = require('jsdom'); 12 | const jquery = require('jquery'); 13 | const diff = require('../cemerick-jsdifflib.js'); 14 | const cookieParser = require('cookie-parser'); 15 | const child_process = require('child_process'); 16 | const captchapng = require('captchapng'); 17 | 18 | const express = require('express'); 19 | const router = express.Router(); 20 | const hostconfig = require('../hostconfig'); 21 | const functions = require('../functions'); 22 | for(var item in functions) global[item] = functions[item]; 23 | const markdown = require('../namumark'); 24 | 25 | const cfg = { 26 | 'wiki.editagree_text': config.getString('wiki.editagree_text'), 27 | 'wiki.front_page': config.getString('wiki.front_page'), 28 | 'wiki.site_name': config.getString('wiki.site_name'), 29 | 'wiki.copyright_url': config.getString('wiki.copyright_url'), 30 | 'wiki.canonical_url': config.getString('wiki.canonical_url'), 31 | 'wiki.copyright_text': config.getString('wiki.copyright_text'), 32 | 'wiki.sitenotice': config.getString('wiki.sitenotice'), 33 | 'wiki.logo_url': config.getString('wiki.logo_url'), 34 | }; 35 | 36 | global.render = async function render(req, title = '', data = {}, error = null, viewName = '', status_redir = 200, cookies = []) { 37 | data.error = error; 38 | 39 | const ret = { 40 | config: cfg, 41 | localConfig: {}, 42 | page: { 43 | viewName, 44 | data, 45 | title, 46 | menus: [], 47 | }, 48 | session: { 49 | menus: [], 50 | member: null, 51 | ip: ip_check(req, 1), 52 | identifier: (islogin(req) ? 'm' : 'i') + ':' + ip_check(req), 53 | }, 54 | }; 55 | 56 | if(typeof status_redir == 'string') { 57 | ret.page.status = 302; 58 | ret.page.location = status_redir; 59 | } else { 60 | ret.page.status = status_redir; 61 | } 62 | 63 | if(islogin(req)) { 64 | var user_document_discuss = null; 65 | const udd = await curs.execute("select tnum, time from threads where namespace = '사용자' and title = ? and status = 'normal'", [req.session.username]); 66 | if(udd.length) user_document_discuss = Math.floor(Number(udd[0].time) / 1000); 67 | 68 | ret.session.member = { 69 | username: req.session.username, 70 | user_document_discuss, 71 | gravatar_url: '', 72 | }; 73 | } 74 | 75 | return ret; 76 | }; 77 | 78 | global.showError = async function showError(req, code, ...params) { 79 | return await render(req, '', { 80 | content: typeof code == 'object' ? (code.msg || fetchErrorString(code.code, code.tag)) : fetchErrorString(code, ...params), 81 | }, _, 'error'); 82 | }; 83 | 84 | module.exports = {}; 85 | 86 | for(var src of fs.readdirSync('./backend', { withFileTypes: true }).filter(dirent => !dirent.isDirectory()).map(dirent => dirent.name)) { 87 | if(src.toLowerCase() == 'backend.js') continue; 88 | var func = eval(fs.readFileSync('./backend/' + src).toString()); 89 | module.exports[func.name] = func; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /backend/unfinished/login_history.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/admin\/login_history$/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | if(!getperm('grant', ip_check(req))) return res.send(await showError(req, 'permission')); 4 | 5 | var error = null; 6 | var content = ` 7 |
8 |
9 | 10 | 11 |
12 | 13 | 14 |
15 | `; 16 | 17 | if(req.method == 'POST') { 18 | var username = req.body['username']; 19 | if(!username) return res.send(await render(req, '로그인 내역', (error = err('alert', { code: 'validator_required', tag: 'username' })) + content, {}, _, error, 'login_history')); 20 | var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]); 21 | if(!data.length) 22 | return res.send(await render(req, '로그인 내역', (error = err('alert', { code: 'invalid_username' })) + content, {}, _, error, 'login_history')); 23 | username = data[0].username; 24 | 25 | const id = rndval('abcdef1234567890', 64); 26 | if(!loginHistory[ip_check(req)]) loginHistory[ip_check(req)] = {}; 27 | var history = await curs.execute("select ip, time from login_history where username = ? order by cast(time as integer) desc limit 50", [username]); 28 | var ua = await curs.execute("select string from useragents where username = ?", [username]); 29 | loginHistory[ip_check(req)][id] = { username, useragent: (ua[0] || { string: '' }).string, history }; 30 | 31 | var logid = 1, lgdata = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 32 | if(lgdata.length) logid = Number(lgdata[0].logid) + 1; 33 | if(!hostconfig.disable_login_history) 34 | insert('block_history', { 35 | date: getTime(), 36 | type: 'login_history', 37 | duration: 0, 38 | note: '', 39 | ismember: islogin(req) ? 'author' : 'ip', 40 | executer: ip_check(req), 41 | target: username, 42 | logid, 43 | }); 44 | 45 | return res.redirect('/admin/login_history/' + id); 46 | } 47 | 48 | return res.send(await render(req, '로그인 내역', content, {}, _, _, 'login_history')); 49 | }); 50 | 51 | router.get(/^\/admin\/login_history\/(.+)$/, async(req, res) => { 52 | const id = req.params[0]; 53 | 54 | if(!loginHistory[ip_check(req)] || (loginHistory[ip_check(req)] && !loginHistory[ip_check(req)][id])) 55 | return res.redirect('/admin/login_history'); 56 | 57 | const { username, history, useragent } = loginHistory[ip_check(req)][id]; 58 | 59 | var content = ` 60 |

마지막 로그인 UA : ${html.escape(useragent)}

61 |

이메일 : ${getUserSetting(username, 'email', '')} 62 | 63 | ${navbtn(0, 0, 0, 0)} 64 | 65 |

66 | 67 | 68 | 69 | 70 | 71 | 72 | `; 73 | 74 | for(var item of history) { 75 | content += ``; 76 | } 77 | 78 | content += ` 79 | 80 |
DateIP
${generateTime(toDate(item.time), timeFormat)}${item.ip}
81 |
82 | ${navbtn(0, 0, 0, 0)} 83 | `; 84 | 85 | return res.send(await render(req, username + ' 로그인 내역', content, {}, _, _, 'login_history')); 86 | }); -------------------------------------------------------------------------------- /routes/login_history.js: -------------------------------------------------------------------------------- 1 | if(ver('4.4.2')) { 2 | 3 | router.all(/^\/admin\/login_history$/, async(req, res, next) => { 4 | if(!['POST', 'GET'].includes(req.method)) return next(); 5 | if(!getperm('login_history', ip_check(req))) return res.send(await showError(req, 'permission')); 6 | 7 | var error = null; 8 | var content = ` 9 |
10 |
11 | 12 | 13 |
14 | 15 | 16 |
17 | `; 18 | 19 | if(req.method == 'POST') { 20 | var username = req.body['username']; 21 | if(!username) return res.send(await render(req, '로그인 내역', (error = err('alert', { code: 'validator_required', tag: 'username' })) + content, {}, _, error, 'login_history')); 22 | var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]); 23 | if(!data.length) 24 | return res.send(await render(req, '로그인 내역', (error = err('alert', { code: 'invalid_username' })) + content, {}, _, error, 'login_history')); 25 | username = data[0].username; 26 | if((hostconfig.owners || []).includes(username) && hostconfig.protect_owners && username != ip_check(req)) 27 | return res.send(await showError(req, 'permission')); 28 | 29 | const id = rndval('abcdef1234567890', 64); 30 | if(!loginHistory[ip_check(req)]) loginHistory[ip_check(req)] = {}; 31 | var history = await curs.execute("select ip, time from login_history where username = ? order by cast(time as integer) desc limit 50", [username]); 32 | var ua = await curs.execute("select string from useragents where username = ?", [username]); 33 | loginHistory[ip_check(req)][id] = { username, useragent: (ua[0] || { string: '' }).string, history }; 34 | 35 | var logid = 1, lgdata = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 36 | if(lgdata.length) logid = Number(lgdata[0].logid) + 1; 37 | if(!hostconfig.disable_login_history) 38 | insert('block_history', { 39 | date: getTime(), 40 | type: 'login_history', 41 | duration: 0, 42 | note: '', 43 | ismember: islogin(req) ? 'author' : 'ip', 44 | executer: ip_check(req), 45 | target: username, 46 | logid, 47 | }); 48 | 49 | return res.redirect('/admin/login_history/' + id); 50 | } 51 | 52 | return res.send(await render(req, '로그인 내역', content, {}, _, _, 'login_history')); 53 | }); 54 | 55 | router.get(/^\/admin\/login_history\/(.+)$/, async(req, res) => { 56 | const id = req.params[0]; 57 | 58 | if(!loginHistory[ip_check(req)] || (loginHistory[ip_check(req)] && !loginHistory[ip_check(req)][id])) 59 | return res.redirect('/admin/login_history'); 60 | 61 | const { username, history, useragent } = loginHistory[ip_check(req)][id]; 62 | 63 | var content = ` 64 |

마지막 로그인 UA : ${html.escape(useragent)}

65 |

이메일 : ${getUserSetting(username, 'email') || ''} 66 | 67 | ${navbtn(0, 0, 0, 0)} 68 | 69 |

70 | 71 | 72 | 73 | 74 | 75 | 76 | `; 77 | 78 | for(var item of history) { 79 | content += ``; 80 | } 81 | 82 | content += ` 83 | 84 |
DateIP
${generateTime(toDate(item.time), timeFormat)}${item.ip}
85 |
86 | ${navbtn(0, 0, 0, 0)} 87 | `; 88 | 89 | return res.send(await render(req, username + ' 로그인 내역', content, {}, _, _, 'login_history')); 90 | }); 91 | 92 | } 93 | -------------------------------------------------------------------------------- /routes/preview.js: -------------------------------------------------------------------------------- 1 | router.post(/^\/preview\/(.*)$/, async(req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | 5 | var skinconfig = skincfgs[getSkin(req)]; 6 | var header = ''; 7 | for(var i=0; i'; 9 | } 10 | for(var i=0; i'; 12 | } 13 | header += skinconfig['additional_heads']; 14 | 15 | res.send(` 16 | 17 | 18 | 19 | 20 | 21 | ${hostconfig.use_external_css ? ` 22 | 23 | 24 | 25 | ` : ` 26 | 27 | 28 | 29 | `} 30 | ${hostconfig.use_external_js ? ` 31 | 32 | 33 | 34 | 35 | 36 | 37 | ` : ` 38 | 39 | 40 | 41 | 42 | 43 | `} 44 | ${header} 45 | 46 | 47 | 48 |

${html.escape(doc + '')}

49 |
50 | ${await markdown(req, req.body['text'], 0, doc + '', 'preview')} 51 |
52 | 53 | 54 | `); 55 | }); 56 | 57 | if(ver('4.20.0')) router.post(/^\/commentpreview$/, async(req, res) => { 58 | const { id } = req.body; 59 | 60 | var content = ``; 61 | content += ` 62 | 75 | `; 76 | 77 | res.send(content); 78 | }); -------------------------------------------------------------------------------- /backend/unfinished/preview.js: -------------------------------------------------------------------------------- 1 | router.post(/^\/preview\/(.*)$/, async(req, res) => { 2 | const title = req.params[0]; 3 | const doc = processTitle(title); 4 | 5 | var skinconfig = skincfgs[getSkin(req)]; 6 | var header = ''; 7 | for(var i=0; i'; 9 | } 10 | for(var i=0; i'; 12 | } 13 | header += skinconfig['additional_heads']; 14 | 15 | res.send(` 16 | 17 | 18 | 19 | 20 | 21 | ${hostconfig.use_external_css ? ` 22 | 23 | 24 | 25 | ` : ` 26 | 27 | 28 | 29 | `} 30 | ${hostconfig.use_external_js ? ` 31 | 32 | 33 | 34 | 35 | 36 | 37 | ` : ` 38 | 39 | 40 | 41 | 42 | 43 | `} 44 | ${header} 45 | 46 | 47 | 48 |

${html.escape(doc + '')}

49 |
50 | ${await markdown(req, req.body['text'], 0, doc + '', 'preview')} 51 |
52 | 53 | 54 | `); 55 | }); 56 | 57 | if(ver('4.20.0')) router.post(/^\/commentpreview$/, async(req, res) => { 58 | const { id } = req.body; 59 | 60 | var content = ``; 61 | content += ` 62 |
75 | `; 76 | 77 | res.send(content); 78 | }); -------------------------------------------------------------------------------- /backend/unfinished/grant.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/admin\/grant$/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | var username = req.query['username']; 4 | if(!getperm('grant', ip_check(req))) return res.send(await showError(req, 'permission')); 5 | 6 | var error = null; 7 | var content = ` 8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 | `; 17 | if(username === undefined) return res.send(await render(req, '권한 부여', content, {}, _, error, 'grant')); 18 | if(!username) return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'validator_required', tag: 'username' })) + content, {}, _, error, 'grant')); 19 | var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]); 20 | if(!data.length) 21 | return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'invalid_username' })) + content, {}, _, error, 'grant')); 22 | username = data[0].username; 23 | 24 | var chkbxs = ''; 25 | for(var prm of perms) { 26 | // if(!getperm('developer', ip_check(req), 1) && 'developer' == (prm)) continue; 27 | chkbxs += ` 28 | ${prm}
29 | `; 30 | } 31 | 32 | content += ` 33 |

사용자 ${html.escape(username)}

34 | 35 |
36 |
37 | ${chkbxs} 38 |
39 | 40 | 41 |
42 | `; 43 | 44 | if(req.method == 'POST') { 45 | if(!username) return res.send(await showError(req, 'invalid_username')); 46 | var data = await curs.execute("select username from users where username = ?", [username]); 47 | if(!data.length) return res.send(await showError(req, 'invalid_username')); 48 | 49 | var prmval = req.body['permissions']; 50 | if(!prmval || !prmval.find) prmval = [prmval]; 51 | 52 | var logstring = ''; 53 | for(var prm of perms) { 54 | // if(!getperm('developer', ip_check(req), 1) && 'developer' == (prm)) continue; 55 | if(getperm(prm, username, 1) && (typeof(prmval.find(item => item == prm)) == 'undefined')) { 56 | logstring += '-' + prm + ' '; 57 | if(permlist[username]) permlist[username].splice(permlist[username].findIndex(item => item == prm), 1); 58 | curs.execute("delete from perms where perm = ? and username = ?", [prm, username]); 59 | } else if(!getperm(prm, username, 1) && (typeof(prmval.find(item => item == prm)) != 'undefined')) { 60 | logstring += '+' + prm + ' '; 61 | if(!permlist[username]) permlist[username] = [prm]; 62 | else permlist[username].push(prm); 63 | curs.execute("insert into perms (perm, username) values (?, ?)", [prm, username]); 64 | } 65 | } 66 | if(!logstring.length) 67 | return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'no_change' })) + content, {}, _, error, 'grant')); 68 | 69 | var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 70 | if(data.length) logid = Number(data[0].logid) + 1; 71 | insert('block_history', { 72 | date: getTime(), 73 | type: 'grant', 74 | note: logstring, 75 | ismember: islogin(req) ? 'author' : 'ip', 76 | executer: ip_check(req), 77 | target: username, 78 | logid, 79 | }); 80 | 81 | return res.redirect('/admin/grant?username=' + encodeURIComponent(username)); 82 | } 83 | 84 | res.send(await render(req, '권한 부여', content, {}, _, _, 'grant')); 85 | }); -------------------------------------------------------------------------------- /backend/unfinished/contribution_document.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/contribution\/(ip|author)\/(.+)\/document$/, async function documentContributionList(req, res) { 2 | const ismember = req.params[0]; 3 | const username = req.params[1]; 4 | var moredata = []; 5 | 6 | if(ismember == 'author' && username.toLowerCase() == 'namubot') { 7 | var data = []; 8 | } else { 9 | var data = await curs.execute("select flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \ 10 | where cast(time as integer) >= ? and ismember = ? " + (username.replace(/\s/g, '') ? "and lower(username) = ?" : "and (lower(username) like '%' || ?)") + " order by cast(time as integer) desc", [ 11 | Number(getTime()) - 2592000000, ismember, username.toLowerCase() 12 | ]); 13 | 14 | // 2018년 더시드 업데이트로 최근 30일을 넘어선 기록을 최대 100개까지 볼 수 있었음 15 | var tt = Number(getTime()) + 12345; 16 | if(data.length) tt = Number(data[data.length - 1].time); 17 | if(data.length < 100 && ver('4.8.0')) 18 | moredata = await curs.execute("select flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \ 19 | where cast(time as integer) < ? and ismember = ? " + (username.replace(/\s/g, '') ? "and lower(username) = ?" : "and (lower(username) like '%' || ?)") + " order by cast(time as integer) desc limit ?", [ 20 | tt, ismember, username.toLowerCase(), 100 - data.length 21 | ]); 22 | data = data.concat(moredata); 23 | } 24 | 25 | var content = ` 26 |

최근 30일동안의 기여 목록 입니다.

27 | 28 |
32 | 33 | 34 | 35 | 36 | ${ver('4.13.0') ? '' : ``} 37 | 38 | 39 | 40 | 41 | 42 | 43 | ${ver('4.13.0') ? '' : ``} 44 | 45 | 46 | 47 | 48 | 49 | `; 50 | 51 | for(var row of data) { 52 | var title = totitle(row.title, row.namespace) + ''; 53 | 54 | content += ` 55 | 0 || row.advance != 'normal' ? ' class=no-line' : '')}> 56 | 79 | 80 | ${ver('4.13.0') ? '' : ` 81 | 84 | `} 85 | 86 | 89 | 90 | `; 91 | 92 | if(row.log.length > 0 || row.advance != 'normal') { 93 | content += ` 94 | 97 | `; 98 | } 99 | } 100 | content += ` 101 | 102 |
문서수정자수정 시간
57 | ${html.escape(title)} 58 | [역사] 59 | ${ 60 | Number(row.rev) > 1 61 | ? '[비교]' 62 | : '' 63 | } 64 | [토론] 65 | 66 | (${row.changes}) 78 | 82 | ${ip_pas(row.username, row.ismember)} 83 | 87 | ${generateTime(toDate(row.time), timeFormat)} 88 |
95 | ${row.log} ${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : ''} 96 |
103 | `; 104 | 105 | res.send(await render(req, `"${username}" 기여 목록`, content, {})); 106 | }); -------------------------------------------------------------------------------- /backend/unfinished/search.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/complete\/(.*)/, (req, res) => { 2 | // 초성검색은 나중에 3 | const query = req.params[0]; 4 | const doc = processTitle(query); 5 | curs.execute("select title, namespace from documents where lower(title) like ? || '%' and lower(namespace) = ? limit 10", [doc.title.toLowerCase(), doc.namespace.toLowerCase()]) 6 | .then(data => { 7 | var ret = []; 8 | for(var i of data) { 9 | ret.push(totitle(i.title, i.namespace) + ''); 10 | } 11 | return res.json(ret); 12 | }) 13 | .catch(e => { 14 | print(e.stack); 15 | return res.status(500).json([]); 16 | }); 17 | }); 18 | 19 | router.get(/^\/go\/(.*)/, (req, res) => { 20 | const query = req.params[0]; 21 | const doc = processTitle(query); 22 | curs.execute("select title, namespace from documents where lower(title) = ? and lower(namespace) = ?", [doc.title.toLowerCase(), doc.namespace.toLowerCase()]) 23 | .then(data => { 24 | if(data.length) return res.redirect('/w/' + totitle(data[0].title, data[0].namespace)); 25 | else return res.redirect('/search/' + query); 26 | }) 27 | .catch(e => { 28 | return res.redirect('/search/' + query); 29 | }); 30 | }); 31 | 32 | router.get(/^\/search\/(.*)/, async(req, res) => { 33 | const query = req.params[0]; 34 | 35 | var content = ` 36 | 48 | `; 49 | 50 | var st = new Date().getTime() / 1000; 51 | 52 | if(!query.replace(/^(\s+)/, '').replace(/(\s+)$/, '')) { 53 | res.send(await render(req, '"' + query + '" 검색 결과', content, {}, _, _, 'search')); 54 | } 55 | 56 | http.request({ 57 | host: hostconfig.search_host, 58 | port: hostconfig.search_port, 59 | path: '/search/' + encodeURIComponent(query) + '?page=' + (req.query['page'] || '1'), 60 | }, async rr => { 61 | var d = ''; 62 | 63 | rr.on('data', function(chunk) { 64 | d += chunk; 65 | }); 66 | 67 | rr.on('end', async function() { 68 | const ret = JSON.parse(d); 69 | var reshtml = ''; 70 | reshtml += ` 71 |
72 | `; 73 | for(var item of ret.result) { 74 | var title = totitle(item.title, item.namespace) + ''; 75 | reshtml += ` 76 |
77 |

78 | 79 | 80 | ${html.escape(title)} 81 | 82 |

83 |
84 | ${item.content} 85 |
86 |
87 | `; 88 | } 89 | reshtml += ` 90 | 105 |
106 | `; 107 | var et = new Date().getTime() / 1000; 108 | content = content + ` 109 |
전체 ${ret.total} 건 / 처리 시간 ${(et - st).toFixed(3).replace(/([0]+)$/, '')}초
110 | ` + reshtml; 111 | res.send(await render(req, '"' + query + '" 검색 결과', content, {}, _, _, 'search')); 112 | }); 113 | }).on('error', async e => { 114 | res.send(await showError(req, 'searchd_fail')); 115 | }).end(); 116 | }); -------------------------------------------------------------------------------- /routes/contribution_document.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/contribution\/(ip|author)\/(.+)\/document$/, async function documentContributionList(req, res) { 2 | const ismember = req.params[0]; 3 | const username = req.params[1]; 4 | var moredata = []; 5 | 6 | if(ismember == 'author' && username.toLowerCase() == 'namubot') { 7 | var data = []; 8 | } else { 9 | var data = await curs.execute("select flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username, loghider from history \ 10 | where cast(time as integer) >= ? and ismember = ? " + (username.replace(/\s/g, '') ? "and lower(username) = ?" : "and (lower(username) like '%' || ?)") + " order by cast(time as integer) desc", [ 11 | Number(getTime()) - 2592000000, ismember, username.toLowerCase() 12 | ]); 13 | 14 | // 2018년 더시드 업데이트로 최근 30일을 넘어선 기록을 최대 100개까지 볼 수 있었음 15 | var tt = Number(getTime()) + 12345; 16 | if(data.length) tt = Number(data[data.length - 1].time); 17 | if(data.length < 100 && ver('4.8.0')) 18 | moredata = await curs.execute("select flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username, loghider from history \ 19 | where cast(time as integer) < ? and ismember = ? " + (username.replace(/\s/g, '') ? "and lower(username) = ?" : "and (lower(username) like '%' || ?)") + " order by cast(time as integer) desc limit ?", [ 20 | tt, ismember, username.toLowerCase(), 100 - data.length 21 | ]); 22 | data = data.concat(moredata); 23 | } 24 | 25 | var content = ` 26 |

최근 30일동안의 기여 목록 입니다.

27 | 28 | 32 | 33 | 34 | 35 | 36 | ${ver('4.13.0') ? '' : ``} 37 | 38 | 39 | 40 | 41 | 42 | 43 | ${ver('4.13.0') ? '' : ``} 44 | 45 | 46 | 47 | 48 | 49 | `; 50 | 51 | for(var row of data) { 52 | var title = totitle(row.title, row.namespace) + ''; 53 | 54 | content += ` 55 | 0 || row.advance != 'normal' ? ' class=no-line' : '')}> 56 | 79 | 80 | ${ver('4.13.0') ? '' : ` 81 | 84 | `} 85 | 86 | 89 | 90 | `; 91 | 92 | if((row.log.length > 0 && !row.loghider) || row.advance != 'normal') { 93 | content += ` 94 | 97 | `; 98 | } 99 | } 100 | content += ` 101 | 102 |
문서수정자수정 시간
57 | ${html.escape(title)} 58 | [역사] 59 | ${ 60 | Number(row.rev) > 1 61 | ? '[비교]' 62 | : '' 63 | } 64 | [토론] 65 | 66 | (${row.changes}) 78 | 82 | ${ip_pas(row.username, row.ismember)} 83 | 87 | ${generateTime(toDate(row.time), timeFormat)} 88 |
95 | ${row.loghider ? '' : row.log} ${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : ''} 96 |
103 | `; 104 | 105 | res.send(await render(req, `"${username}" 기여 목록`, content, {})); 106 | }); -------------------------------------------------------------------------------- /routes/suspend_account.js: -------------------------------------------------------------------------------- 1 | if(!ver('4.18.0')) router.all(/^\/admin\/suspend_account$/, async(req, res) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | if(!hasperm(req, 'suspend_account')) return res.status(403).send(await showError(req, 'permission')); 4 | 5 | var content = ` 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 22 |
23 | 24 | 25 |
26 | `; 27 | 28 | var error = null; 29 | 30 | if(req.method == 'POST') do { 31 | var { expire, note, username } = req.body; 32 | if(!username) { content = (error = err('alert', { code: 'validator_required', tag: 'username' })) + content; break; } 33 | if((hostconfig.owners || []).includes(username)) { content = (error = err('alert', { code: 'invalid_permission' })) + content; break; } 34 | var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]); 35 | if(!data.length) { content = (error = err('alert', { code: 'invalid_username' })) + content; break; } 36 | username = data[0].username; 37 | if(!note) { content = (error = err('alert', { code: 'validator_required', tag: 'note' })) + content; break; } 38 | if(!expire) { content = (error = err('alert', { code: 'validator_required', tag: 'expire' })) + content; break; } 39 | if(isNaN(Number(expire))) { content = (error = err('alert', { code: 'invalid_type_number', tag: 'expire' })) + content; break; } 40 | if(Number(expire) > 29030400) { content = (error = err('alert', { msg: 'expire의 값은 29030400 이하이어야 합니다.' })) + content; break; } 41 | if(expire == '-1') { 42 | if(!(await userblocked(username))) { content = (error = err('alert', { code: 'already_unsuspend_account' })) + content; break; } 43 | curs.execute("delete from suspend_account where username = ?", [username]); 44 | var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 45 | if(data.length) logid = Number(data[0].logid) + 1; 46 | insert('block_history', { 47 | date: getTime(), 48 | type: 'suspend_account', 49 | duration: '-1', 50 | note, 51 | ismember: islogin(req) ? 'author' : 'ip', 52 | executer: ip_check(req), 53 | target: username, 54 | logid, 55 | }); 56 | return res.redirect('/admin/suspend_account'); 57 | } 58 | if(await userblocked(username)) { content = (error = err('alert', { code: 'already_suspend_account' })) + content; break; } 59 | const date = getTime(); 60 | const expiration = expire == '0' ? '0' : String(Number(date) + Number(expire) * 1000); 61 | 62 | curs.execute("insert into suspend_account (username, date, expiration, note) values (?, ?, ?, ?)", [username, String(getTime()), expiration, note]); 63 | var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 64 | if(data.length) logid = Number(data[0].logid) + 1; 65 | insert('block_history', { 66 | date: getTime(), 67 | type: 'suspend_account', 68 | duration: expire, 69 | note, 70 | ismember: islogin(req) ? 'author' : 'ip', 71 | executer: ip_check(req), 72 | target: username, 73 | logid, 74 | }); 75 | 76 | return res.redirect('/admin/suspend_account'); 77 | } while(0); 78 | 79 | return res.send(await render(req, '사용자 차단', content, {}, '', error, 'suspend_account')); 80 | }); -------------------------------------------------------------------------------- /backend/unfinished/suspend_account.js: -------------------------------------------------------------------------------- 1 | if(!ver('4.18.0')) router.all(/^\/admin\/suspend_account$/, async(req, res) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | if(!hasperm(req, 'suspend_account')) return res.status(403).send(await showError(req, 'permission')); 4 | 5 | var content = ` 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 22 |
23 | 24 | 25 |
26 | `; 27 | 28 | var error = null; 29 | 30 | if(req.method == 'POST') do { 31 | var { expire, note, username } = req.body; 32 | if(!username) { content = (error = err('alert', { code: 'validator_required', tag: 'username' })) + content; break; } 33 | if((hostconfig.owners || []).includes(username)) { content = (error = err('alert', { code: 'invalid_permission' })) + content; break; } 34 | var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]); 35 | if(!data.length) { content = (error = err('alert', { code: 'invalid_username' })) + content; break; } 36 | username = data[0].username; 37 | if(!note) { content = (error = err('alert', { code: 'validator_required', tag: 'note' })) + content; break; } 38 | if(!expire) { content = (error = err('alert', { code: 'validator_required', tag: 'expire' })) + content; break; } 39 | if(isNaN(Number(expire))) { content = (error = err('alert', { code: 'invalid_type_number', tag: 'expire' })) + content; break; } 40 | if(Number(expire) > 29030400) { content = (error = err('alert', { msg: 'expire의 값은 29030400 이하이어야 합니다.' })) + content; break; } 41 | if(expire == '-1') { 42 | if(!(await userblocked(username))) { content = (error = err('alert', { code: 'already_unsuspend_account' })) + content; break; } 43 | curs.execute("delete from suspend_account where username = ?", [username]); 44 | var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 45 | if(data.length) logid = Number(data[0].logid) + 1; 46 | insert('block_history', { 47 | date: getTime(), 48 | type: 'suspend_account', 49 | duration: '-1', 50 | note, 51 | ismember: islogin(req) ? 'author' : 'ip', 52 | executer: ip_check(req), 53 | target: username, 54 | logid, 55 | }); 56 | return res.redirect('/admin/suspend_account'); 57 | } 58 | if(await userblocked(username)) { content = (error = err('alert', { code: 'already_suspend_account' })) + content; break; } 59 | const date = getTime(); 60 | const expiration = expire == '0' ? '0' : String(Number(date) + Number(expire) * 1000); 61 | 62 | curs.execute("insert into suspend_account (username, date, expiration, note) values (?, ?, ?, ?)", [username, String(getTime()), expiration, note]); 63 | var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 64 | if(data.length) logid = Number(data[0].logid) + 1; 65 | insert('block_history', { 66 | date: getTime(), 67 | type: 'suspend_account', 68 | duration: expire, 69 | note, 70 | ismember: islogin(req) ? 'author' : 'ip', 71 | executer: ip_check(req), 72 | target: username, 73 | logid, 74 | }); 75 | 76 | return res.redirect('/admin/suspend_account'); 77 | } while(0); 78 | 79 | return res.send(await render(req, '사용자 차단', content, {}, '', error, 'suspend_account')); 80 | }); -------------------------------------------------------------------------------- /routes/delete_account.js: -------------------------------------------------------------------------------- 1 | if(hostconfig.allow_account_deletion) router.all(/^\/member\/delete_account$/, async(req, res, next) => { 2 | if(!['GET', 'POST'].includes(req.method)) return next(); 3 | if(!islogin(req)) return res.redirect('/member/login?redirect=%2Fmember%2Fdelete_account'); 4 | const username = ip_check(req); 5 | var error = false; 6 | 7 | var { password } = (await curs.execute("select password from users where username = ?", [username]))[0]; 8 | 9 | var content = ` 10 |
11 |

계정을 삭제하면 문서 역사에서 당신의 사용자 이름이 익명화됩니다. 문서 배포 라이선스가 퍼블릭 도메인이 아닌 경우 가급적 탈퇴는 자제해주세요.

12 | 13 |
14 | 15 | 16 | ${!error && req.method == 'POST' && req.body['username'] != username ? (error = true, `

자신의 사용자 이름을 입력해주세요.

`) : ''} 17 |
18 | 19 |
20 | 21 | 22 | ${!error && req.method == 'POST' && sha3(req.body['password'] + '') != password ? (error = true, `

비밀번호를 확인해주세요.

`) : ''} 23 |
24 | 25 |
26 | 취소 27 | 취소 28 | 취소 29 | 취소 30 | 31 | 취소 32 | 취소 33 |
34 |
35 | `; 36 | 37 | if(req.method == 'POST' && !error) { 38 | curs.execute("delete from users where username = ?", [username]); 39 | curs.execute("delete from perms where username = ?", [username]); 40 | curs.execute("delete from suspend_account where username = ?", [username]); 41 | curs.execute("delete from user_settings where username = ?", [username]); 42 | curs.execute("delete from acl where title = ? and namespace = '사용자'", [username]); 43 | curs.execute("delete from classic_acl where title = ? and namespace = '사용자'", [username]); 44 | curs.execute("delete from documents where title = ? and namespace = '사용자'", [username]); 45 | curs.execute("delete from history where title = ? and namespace = '사용자'", [username]); 46 | curs.execute("delete from login_history where username = ?", [username]); 47 | curs.execute("delete from stars where username = ?", [username]); 48 | curs.execute("delete from useragents where username = ?", [username]); 49 | curs.execute("update history set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]); 50 | curs.execute("update res set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]); 51 | curs.execute("update res set hider = '탈퇴한 사용자' where hider = ?", [username]); 52 | curs.execute("update block_history set executer = '탈퇴한 사용자', ismember = 'ip' where executer = ? and ismember = 'author'", [username]); 53 | curs.execute("update block_history set target = '탈퇴한 사용자' where target = ?", [username]); 54 | curs.execute("update edit_requests set processor = '탈퇴한 사용자', ismember = 'ip' where processor = ? and ismember = 'author'", [username]); 55 | curs.execute("update edit_requests set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]); 56 | delete req.session.username; 57 | delete userset[username]; 58 | if(permlist[username]) permlist[username] = []; 59 | res.cookie('honoka', '', { expires: new Date(Date.now() - 1) }); 60 | return res.send(await render(req, '계정 삭제', ` 61 |

${html.escape(username)}님 안녕히 가십시오.

62 | `, {}, _, false, 'delete_account')); 63 | } 64 | 65 | return res.send(await render(req, '계정 삭제', content, {}, _, error, 'delete_account')); 66 | }); -------------------------------------------------------------------------------- /backend/unfinished/delete_account.js: -------------------------------------------------------------------------------- 1 | if(hostconfig.allow_account_deletion) router.all(/^\/member\/delete_account$/, async(req, res, next) => { 2 | if(!['GET', 'POST'].includes(req.method)) return next(); 3 | if(!islogin(req)) return res.redirect('/member/login?redirect=%2Fmember%2Fdelete_account'); 4 | const username = ip_check(req); 5 | var error = false; 6 | 7 | var { password } = (await curs.execute("select password from users where username = ?", [username]))[0]; 8 | 9 | var content = ` 10 |
11 |

계정을 삭제하면 문서 역사에서 당신의 사용자 이름이 익명화됩니다. 문서 배포 라이선스가 퍼블릭 도메인이 아닌 경우 가급적 탈퇴는 자제해주세요.

12 | 13 |
14 | 15 | 16 | ${!error && req.method == 'POST' && req.body['username'] != username ? (error = true, `

자신의 사용자 이름을 입력해주세요.

`) : ''} 17 |
18 | 19 |
20 | 21 | 22 | ${!error && req.method == 'POST' && sha3(req.body['password'] + '') != password ? (error = true, `

비밀번호를 확인해주세요.

`) : ''} 23 |
24 | 25 |
26 | 취소 27 | 취소 28 | 취소 29 | 취소 30 | 31 | 취소 32 | 취소 33 |
34 |
35 | `; 36 | 37 | if(req.method == 'POST' && !error) { 38 | curs.execute("delete from users where username = ?", [username]); 39 | curs.execute("delete from perms where username = ?", [username]); 40 | curs.execute("delete from suspend_account where username = ?", [username]); 41 | curs.execute("delete from user_settings where username = ?", [username]); 42 | curs.execute("delete from acl where title = ? and namespace = '사용자'", [username]); 43 | curs.execute("delete from classic_acl where title = ? and namespace = '사용자'", [username]); 44 | curs.execute("delete from documents where title = ? and namespace = '사용자'", [username]); 45 | curs.execute("delete from history where title = ? and namespace = '사용자'", [username]); 46 | curs.execute("delete from login_history where username = ?", [username]); 47 | curs.execute("delete from stars where username = ?", [username]); 48 | curs.execute("delete from useragents where username = ?", [username]); 49 | curs.execute("update history set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]); 50 | curs.execute("update res set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]); 51 | curs.execute("update res set hider = '탈퇴한 사용자' where hider = ?", [username]); 52 | curs.execute("update block_history set executer = '탈퇴한 사용자', ismember = 'ip' where executer = ? and ismember = 'author'", [username]); 53 | curs.execute("update block_history set target = '탈퇴한 사용자' where target = ?", [username]); 54 | curs.execute("update edit_requests set processor = '탈퇴한 사용자', ismember = 'ip' where processor = ? and ismember = 'author'", [username]); 55 | curs.execute("update edit_requests set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]); 56 | delete req.session.username; 57 | delete userset[username]; 58 | if(permlist[username]) permlist[username] = []; 59 | res.cookie('honoka', '', { expires: new Date(Date.now() - 1) }); 60 | return res.send(await render(req, '계정 삭제', ` 61 |

${html.escape(username)}님 안녕히 가십시오.

62 | `, {}, _, false, 'delete_account')); 63 | } 64 | 65 | return res.send(await render(req, '계정 삭제', content, {}, _, error, 'delete_account')); 66 | }); -------------------------------------------------------------------------------- /backend/unfinished/recent_discuss.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/RecentDiscuss$/, async function recentDicsuss(req, res) { 2 | var logtype = req.query['logtype']; 3 | if(!logtype) logtype = 'all'; 4 | 5 | var content = ` 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | `; 30 | 31 | var trds; 32 | 33 | switch(logtype) { 34 | case 'normal_thread': 35 | trds = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'normal' and not deleted = '1' order by cast(time as integer) desc limit 120"); 36 | break; case 'old_thread': 37 | trds = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'normal' and not deleted = '1' order by cast(time as integer) asc limit 120"); 38 | break; case 'closed_thread': 39 | trds = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'close' and not deleted = '1' order by cast(time as integer) desc limit 120"); 40 | break; case 'open_editrequest': 41 | trds = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'open' and not deleted = '1' order by cast(date as integer) desc limit 120"); 42 | break; case 'closed_editrequest': 43 | trds = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'closed' and not deleted = '1' order by cast(date as integer) desc limit 120"); 44 | break; case 'accepted_editrequest': 45 | trds = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'accepted' and not deleted = '1' order by cast(date as integer) desc limit 120"); 46 | break; default: 47 | var data1 = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'normal' and not deleted = '1' order by cast(time as integer) desc limit 120"); 48 | var data2 = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'open' and not deleted = '1' order by cast(date as integer) desc limit 120"); 49 | trds = data1.concat(data2).sort((l, r) => ((r.date || r.time) - (l.date || l.time))).slice(0, 120); 50 | } 51 | 52 | for(var trd of trds) { 53 | const title = totitle(trd.title, trd.namespace) + ''; 54 | 55 | content += ` 56 | 57 | 63 | 64 | 67 | 68 | `; 69 | } 70 | content += ` 71 | 72 |
항목수정 시간
58 | ${trd.state 59 | ? `편집 요청 ${html.escape(minor >= 16 ? trd.slug : trd.id)} (${html.escape(title)})` 60 | : `${html.escape(trd.topic)} (${html.escape(title)})` 61 | } 62 | 65 | ${generateTime(toDate(trd.time || trd.date), timeFormat)} 66 |
73 | `; 74 | 75 | res.send(await render(req, '최근 토론', content, {})); 76 | }); -------------------------------------------------------------------------------- /backend/unfinished/session.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/member\/logout$/, async(req, res, next) => { 2 | var autologin; 3 | if(autologin = req.cookies['honoka']) { 4 | await curs.execute("delete from autologin_tokens where token = ?", [autologin]); 5 | res.cookie('honoka', '', { expires: new Date(Date.now() - 1) }); 6 | } 7 | var desturl = req.query['redirect']; 8 | if(!desturl) desturl = '/'; 9 | delete req.session.username; 10 | res.redirect(desturl); 11 | }); 12 | 13 | router.all(/^\/member\/login$/, async function loginScreen(req, res, next) { 14 | if(!['GET', 'POST'].includes(req.method)) return next(); 15 | 16 | var desturl = req.query['redirect']; 17 | if(!desturl) desturl = '/'; 18 | 19 | if(islogin(req)) return res.redirect(desturl); 20 | 21 | var id = '1', pw = '1'; 22 | 23 | var error = null; 24 | 25 | if(req.method == 'POST') do { 26 | id = req.body['username'] || ''; 27 | pw = req.body['password'] || ''; 28 | if(!id) break; 29 | var data = await curs.execute("select username from users where lower(username) = ? COLLATE NOCASE", [id.toLowerCase()]); 30 | var invalidusername = !id || !data.length; 31 | if(invalidusername) break; 32 | var usr = data; 33 | if(!pw) break; 34 | var data = await curs.execute("select username, password from users where lower(username) = ? and password = ? COLLATE NOCASE", [id.toLowerCase(), sha3(pw)]); 35 | var invalidpw = !invalidusername && (!data.length || !pw); 36 | if(invalidpw) break; 37 | var blocked = ver('4.1.0') ? 0 : await userblocked(id); 38 | if(blocked) break; 39 | } while(0); 40 | 41 | var content = ` 42 | 69 | `; 70 | 71 | if(req.method == 'POST' && !error) { 72 | id = usr[0].username; 73 | if(req.body['autologin']) { 74 | const key = rndval('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/', 128); 75 | res.cookie('honoka', key, { 76 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 360), 77 | httpOnly: true, 78 | }); 79 | await curs.execute("insert into autologin_tokens (username, token) values (?, ?)", [id, key]); 80 | } 81 | 82 | if(!hostconfig.disable_login_history) { 83 | curs.execute("insert into login_history (username, ip, time) values (?, ?, ?)", [id, ip_check(req, 1), getTime()]); 84 | conn.run("delete from useragents where username = ?", [id], () => { 85 | curs.execute("insert into useragents (username, string) values (?, ?)", [id, req.headers['user-agent']]); 86 | }); 87 | } 88 | 89 | req.session.username = id; 90 | return res.redirect(desturl); 91 | } 92 | 93 | res.send(await render(req, '로그인', content, {}, _, error, 'login')); 94 | }); -------------------------------------------------------------------------------- /routes/session.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/member\/logout$/, async(req, res, next) => { 2 | var autologin; 3 | if(autologin = req.cookies['honoka']) { 4 | await curs.execute("delete from autologin_tokens where token = ?", [sha3(autologin)]); 5 | res.cookie('honoka', '', { expires: new Date(Date.now() - 1) }); 6 | } 7 | var desturl = req.query['redirect']; 8 | if(!desturl) desturl = '/'; 9 | delete req.session.username; 10 | res.redirect(desturl); 11 | }); 12 | 13 | router.all(/^\/member\/login$/, async function loginScreen(req, res, next) { 14 | if(!['GET', 'POST'].includes(req.method)) return next(); 15 | 16 | var desturl = req.query['redirect']; 17 | if(!desturl) desturl = '/'; 18 | 19 | if(islogin(req)) return res.redirect(desturl); 20 | 21 | var id = '1', pw = '1'; 22 | 23 | var error = null; 24 | 25 | if(req.method == 'POST') do { 26 | id = req.body['username'] || ''; 27 | pw = req.body['password'] || ''; 28 | if(!id) break; 29 | var data = await curs.execute("select username from users where lower(username) = ? COLLATE NOCASE", [id.toLowerCase()]); 30 | var invalidusername = !id || !data.length; 31 | if(invalidusername) break; 32 | var usr = data; 33 | if(!pw) break; 34 | var data = await curs.execute("select username, password from users where lower(username) = ? and password = ? COLLATE NOCASE", [id.toLowerCase(), sha3(pw)]); 35 | var invalidpw = !invalidusername && (!data.length || !pw); 36 | if(invalidpw) break; 37 | var blocked = ver('4.1.0') ? 0 : await userblocked(id); 38 | if(blocked) break; 39 | } while(0); 40 | 41 | var content = ` 42 | 69 | `; 70 | 71 | if(req.method == 'POST' && !error) { 72 | id = usr[0].username; 73 | if(req.body['autologin']) { 74 | const key = rndval('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/', 128); 75 | res.cookie('honoka', key, { 76 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 360), 77 | httpOnly: true, 78 | secure: hostconfig.sessionhttps, 79 | samesite: "lax" 80 | }); 81 | await curs.execute("insert into autologin_tokens (username, token) values (?, ?)", [id, sha3(key)]); 82 | } 83 | 84 | if(!hostconfig.disable_login_history) { 85 | curs.execute("insert into login_history (username, ip, time) values (?, ?, ?)", [id, ip_check(req, 1), getTime()]); 86 | conn.run("delete from useragents where username = ?", [id], () => { 87 | curs.execute("insert into useragents (username, string) values (?, ?)", [id, req.headers['user-agent']]); 88 | }); 89 | } 90 | 91 | req.session.username = id; 92 | return res.redirect(desturl); 93 | } 94 | 95 | res.send(await render(req, '로그인', content, {}, _, error, 'login')); 96 | }); -------------------------------------------------------------------------------- /routes/recent_discuss.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/RecentDiscuss$/, async function recentDicsuss(req, res) { 2 | var logtype = req.query['logtype']; 3 | if(!logtype) logtype = 'all'; 4 | 5 | var content = ` 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | `; 30 | 31 | var trds; 32 | 33 | switch(logtype) { 34 | case 'normal_thread': 35 | trds = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'normal' and not deleted = '1' order by cast(time as integer) desc limit 120"); 36 | break; case 'old_thread': 37 | trds = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'normal' and not deleted = '1' order by cast(time as integer) asc limit 120"); 38 | break; case 'closed_thread': 39 | trds = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'close' and not deleted = '1' order by cast(time as integer) desc limit 120"); 40 | break; case 'open_editrequest': 41 | trds = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'open' and not deleted = '1' order by cast(date as integer) desc limit 120"); 42 | break; case 'closed_editrequest': 43 | trds = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'closed' and not deleted = '1' order by cast(date as integer) desc limit 120"); 44 | break; case 'accepted_editrequest': 45 | trds = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'accepted' and not deleted = '1' order by cast(date as integer) desc limit 120"); 46 | break; default: { 47 | if(ver('4.18.1')) { 48 | trds = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'normal' and not deleted = '1' order by cast(time as integer) desc limit 120"); 49 | } else { 50 | var data1 = await curs.execute("select title, namespace, topic, time, tnum, slug from threads where status = 'normal' and not deleted = '1' order by cast(time as integer) desc limit 120"); 51 | var data2 = await curs.execute("select id, slug, title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where state = 'open' and not deleted = '1' order by cast(date as integer) desc limit 120"); 52 | trds = data1.concat(data2).sort((l, r) => ((r.date || r.time) - (l.date || l.time))).slice(0, 120); 53 | } 54 | } 55 | } 56 | 57 | for(var trd of trds) { 58 | const title = totitle(trd.title, trd.namespace) + ''; 59 | 60 | content += ` 61 | 62 | 68 | 69 | 72 | 73 | `; 74 | } 75 | content += ` 76 | 77 |
항목수정 시간
63 | ${trd.state 64 | ? `편집 요청 ${html.escape(ver('4.16.0') ? trd.slug : trd.id)} (${html.escape(title)})` 65 | : `${html.escape(trd.topic)} (${html.escape(title)})` 66 | } 67 | 70 | ${generateTime(toDate(trd.time || trd.date), timeFormat)} 71 |
78 | `; 79 | 80 | res.send(await render(req, '최근 토론', content, {})); 81 | }); 82 | -------------------------------------------------------------------------------- /backend/wiki.js: -------------------------------------------------------------------------------- 1 | (async function viewDocument(req, title) { 2 | const doc = processTitle(title); 3 | var { rev } = req.query; 4 | 5 | if(rev) { 6 | var rawContent = await curs.execute("select content, time from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]); 7 | var data = rawContent; 8 | } else { 9 | rev = null; 10 | var rawContent = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 11 | } 12 | if(rev && !rawContent.length) return await showError(req, 'revision_not_found'); 13 | 14 | var content = {}; 15 | var httpstat = 200; 16 | var viewname = 'wiki'; 17 | var error = null; 18 | var lastedit = undefined; 19 | 20 | const aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 21 | if(aclmsg) { 22 | if(!ver('4.5.7')) return await showError(req, 'permission_read'); 23 | httpstat = 403; 24 | error = err('error', { code: 'permission_read', msg: aclmsg }); 25 | content.content = '

' + aclmsg + '

'; 26 | } else if(!rawContent.length) { 27 | viewname = 'notfound'; 28 | httpstat = 404; 29 | var data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \ 30 | where title = ? and namespace = ? order by cast(rev as integer) desc limit 3", 31 | [doc.title, doc.namespace]); 32 | 33 | content.history = []; 34 | 35 | if(data.length) { 36 | for(var row of data) { 37 | var historylog = { 38 | date: row.time, 39 | rev: row.rev, 40 | logtype: row.advance, 41 | count: row.changes, 42 | target_rev: row.flags.split('\n')[0], 43 | log: row.log, 44 | }; 45 | historylog[row.ismember] = row.username; 46 | content.history.push(historylog); 47 | } 48 | } 49 | } else { 50 | if(rawContent[0].content.startsWith('#redirect ')) { 51 | const nd = rawContent[0].content.split('\n')[0].replace('#redirect ', '').split('#'); 52 | const ntitle = nd[0]; 53 | 54 | if(req.query['noredirect'] != '1' && !req.query['from']) { 55 | return await render(req, totitle(doc.title, doc.namespace) + '', { 56 | redirect: ntitle, 57 | }, error, 'w', 302); 58 | } else { 59 | content = '#redirect ' + html.escape(ntitle) + ''; 60 | } 61 | } else content = await markdown(req, rawContent[0].content, 0, doc + ''); 62 | 63 | if(rev && ver('4.20.0') && hostconfig.namuwiki_exclusive) content = alertBalloon('[주의!] 문서의 이전 버전(' + generateTime(toDate(data[0].time), timeFormat) + '에 수정)을 보고 있습니다. 최신 버전으로 이동', 'danger', true, '', 1) + content; 64 | if(req.query['from']) { 65 | content = alertBalloon('' + html.escape(req.query['from']) + '에서 넘어옴', 'info', false) + content; 66 | } 67 | 68 | var data = await curs.execute("select time from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 69 | lastedit = Number(data[0].time); 70 | } 71 | 72 | const dpg = await curs.execute("select tnum, time from threads where namespace = ? and title = ? and status = 'normal' and cast(time as integer) >= ?", [doc.namespace, doc.title, getTime() - 86400000]); 73 | 74 | var star_count = 0, starred = false; 75 | if(rawContent.length) { 76 | var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]); 77 | if(dbdata.length) starred = true; 78 | var dd = await curs.execute("select count(title) from stars where title = ? and namespace = ?", [doc.title, doc.namespace]); 79 | star_count = dd[0]['count(title)']; 80 | } 81 | 82 | return(await render(req, totitle(doc.title, doc.namespace) + (rev ? (' (r' + rev + ' 판)') : ''), { 83 | content, 84 | star_count: ver('4.9.0') && rawContent.length ? star_count : undefined, 85 | starred: ver('4.9.0') && rawContent.length ? starred : undefined, 86 | date: Math.floor(lastedit / 1000), 87 | document: doc, 88 | rev, 89 | user: doc.namespace == '사용자' ? true : false, 90 | discuss_progress: dpg.length ? true : false, 91 | }, error, viewname, httpstat)); 92 | }); -------------------------------------------------------------------------------- /backend/unfinished/history.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/history\/(.*)/, async function viewHistory(req, res) { 2 | var title = req.params[0]; 3 | const doc = processTitle(title); 4 | title = totitle(doc.title, doc.namespace); 5 | 6 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 7 | if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg })); 8 | 9 | var total = (await curs.execute("select count(rev) from history where title = ? and namespace = ?", [doc.title, doc.namespace]))[0]['count(rev)']; 10 | var data; 11 | const from = req.query['from']; 12 | const until = req.query['until']; 13 | if(from) { 14 | data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username, edit_request_id from history \ 15 | where title = ? and namespace = ? and (cast(rev as integer) <= ? AND cast(rev as integer) > ?) \ 16 | order by cast(rev as integer) desc", 17 | [doc.title, doc.namespace, Number(from), Number(from) - 30]); 18 | } else if(until) { 19 | data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username, edit_request_id from history \ 20 | where title = ? and namespace = ? and (cast(rev as integer) >= ? AND cast(rev as integer) < ?) \ 21 | order by cast(rev as integer) desc", 22 | [doc.title, doc.namespace, Number(until), Number(until) + 30]); 23 | } else { 24 | data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username, edit_request_id from history \ 25 | where title = ? and namespace = ? order by cast(rev as integer) desc limit 30", 26 | [doc.title, doc.namespace]); 27 | } 28 | if(!data.length) return res.send(await showError(req, 'document_not_found')); 29 | 30 | const navbtns = navbtn(total, data[data.length-1].rev, data[0].rev, '/history/' + encodeURIComponent(title)); 31 | var content = ` 32 |

33 | 34 |

35 | 36 | ${navbtns} 37 | 38 |
    39 | `; 40 | 41 | for(var row of data) { 42 | const erq = row.edit_request_id; 43 | if(erq && ver('4.16.0')) { 44 | var dbd = await curs.execute("select slug from edit_requests where id = ?", [erq]); 45 | if(dbd.length) erq = dbd[0].slug; 46 | } 47 | content += ` 48 |
  • 49 | ${generateTime(toDate(row.time), timeFormat)} 50 | 51 | 52 | (보기 | 53 | RAW | 54 | Blame | 55 | 이 리비젼으로 되돌리기${ 56 | Number(row.rev) > 1 57 | ? ' | 비교' 58 | : '' 59 | }) 60 | 61 | 62 | 63 | 64 | 65 | ${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : ''} 66 | 67 | r${row.rev} 68 | 69 | (${row.changes}) 81 | 82 | ${row.edit_request_id ? '(편집 요청)' : ''} ${ip_pas(row.username, row.ismember)} 83 | 84 | (${row.log}) 85 |
  • 86 | `; 87 | } 88 | 89 | content += ` 90 |
91 | 92 | ${navbtns} 93 | 94 | 95 | `; 96 | 97 | res.send(await render(req, totitle(doc.title, doc.namespace) + '의 역사', content, { 98 | document: doc, 99 | }, '', null, 'history')); 100 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## **참고!** 2 | **이 엔진은 the seed 옛 프론트엔드를 중점적으로 모방하는 프로젝트이다. 지금과는 꽤 이질적이었으며, SPA가 사용되지 않았다. 나무위키, 알파위키 또는 더시드위키를 2020년 3월 이후에 처음 접했으면 '내가 생각하는 더 시드 엔진이 아닌데?'라고 생각하는 것이 당연할 것이다.** 3 | 4 | ## 개요 5 | 더시드 엔진 (구 프론트엔드 기준) 모방 프로젝트. 6 | < "엔진 내부 UI는 상관없음." ( https://feedback.theseed.io/posts/280 ) > 7 | 8 | [onamu-theseed](https://github.com/gdl-blue/onamu-theseed)의 후속이다. 9 | 10 | 이 엔진이 정상 작동하는 것으로 확인된 Node.js 버전은 8.6.0, 12.16.2, 12.18.1, 16.6.2이다. 만약 혹시나 Windows XP/Vista에서 실행이 필요한 경우 [이것](https://github.com/hlizard/node8-xp/raw/v8.6.0-xp/Release/Release.zip)을 사용할 것. 11 | 12 | 테스트 서버: 13 | - [내 서버](https://go2021.glitch.me) - the seed 4.11.2 14 | - ~~[test님의 서버](https://seore.org) - the seed 4.20.0, Nuxt.js로 포팅됨~~ 15 | - [테스트위키](https://testwiki.kr) - the seed 4.22.9 16 | 17 | 나무픽스와 거의 호환된다. 18 | 19 | Pull Request 시 서버 코드에는 `?.`, `??`, `import` 등의 신문법, 프론트엔드 자바스크립트에는 ES6 이상 문법을 사용하지 말 것. 20 | 21 | 파서 함수 이름이 마크다운인 이유는 개발 초기에는 마크다운을 사용했기 때문이다. 22 | 23 | ## 기초 사용 방법 24 | - **만약 config.json에서 `use_external_js`과 `use_external_css`이 true이면 아래 단계는 생략해도 된다.** 스킨만 추가하면 된다. 25 | - css, js 디렉토리를 만든다. 26 | - https://theseed.io/js/theseed.js, https://theseed.io/js/jquery-2.1.4.min.js, https://theseed.io/js/jquery-1.11.3.min.js, https://theseed.io/js/intersection-observer.js, https://theseed.io/js/dateformatter.js )를 각각 다운로드받아 js 디렉토리에 복사한다. 27 | - https://theseed.io/css/wiki.css, https://theseed.io/css/katex.min.css, https://theseed.io/css/diffview.css )를 각각 다운로드받아 css 디렉토리에 복사한다. 28 | - skins 디렉토리를 만든다. 29 | - [buma](https://github.com/LiteHell/theseed-skin-buma/tree/d77eef50a77007da391c5082b4b94818db372417), [liberty](https://github.com/namuwiki/theseed-skin-liberty/tree/153cf78f70206643ec42e856aff8280dc21eb2c0) 등 원하는 스킨을 내려받고 skins 디렉토리에 스킨 이름으로 하위디렉토리를 만들어 복사한다. 30 | - `npm i`를 실행한다. 31 | - `node server`를 실행한다. 32 | 33 | ## 이메일 설정법 [Gmail] 34 | - 먼저 "자신사이트주소/admin/config" 에 접속해 `사이트 주소`란에 자신의 사이트주소를 입력한다. 35 | - config.json 파일을 열고 `"disable_email":"true"`를 제거한다. 36 | - `"mailhost":"smtp.gmail.com","email":"인증메일을 보낼 Gmail 주소","passwd":"구글 앱 비밀번호"`를 추가한다. 37 | - [[구글 앱 비밀번호 설정링크]](https://myaccount.google.com/apppasswords) 38 | - 타사메일의 경우 smtp.gmail.com을 타사메일의 smtp주소로 변경해야함. 39 | 40 | ## 추가 도구 41 | - backlink-reset.js: 역링크 초기화 42 | - undelete-thread.js: 삭제된 토론 복구 43 | - namuwiki-importer.js: 나무위키 데이타베이스 덤프 가져오기 44 | 45 | ## config.json 46 | - config.json 수정으로 숨겨진 설정을 제어할 수 있다. 47 | - `disable_email`: (기본값 false) 전자우편 인증을 끈다. 48 | - `disable_login_history`: (기본값 false) 로그인 내역을 기록하지 않게 한다. 49 | - `use_external_js`: (기본값 false) theseed.js, jQuery 등을 [theseed.io](https://theseed.io)에서 불러온다. 50 | - `use_external_css`: (기본값 false) wiki.css 등을 [theseed.io](https://theseed.io)에서 불러온다. 51 | - `allow_account_deletion`: (기본값 false) 계정 탈퇴를 허용한다. 52 | - `allow_account_rename`: (기본값 false) 닉네임 변경을 허용한다. 53 | - `search_host`: (기본값 "127.5.5.5") 검색 서버 호스트 주소 54 | - `search_port`: (기본값 25005) 검색 서버 포트 55 | - `search_autostart`: (기본값 false) 같은 디렉토리에 검색 서버 프로그램(search.js)이 있을 경우 위키 서버 시작 시 검색 서버를 같이 시작시킨다. 56 | - `no_username_format`: (기본값 false) 한글, 공백 등의 특수문자를 사용자 이름으로 쓸 수 있게 하고 길이 제한을 없앤다. 57 | - `owners`: (기본값 \[\]) /admin/config에 접속할 수 있는 사용자 이름 배열 58 | - `reserved_usernames`: (기본값 \["namubot"\]) 이 배열 안에 있는 닉네임으로 계정을 만들 수 없다. 59 | - `theseed_version`: (기본값 "4.12.0") [the seed 판올림 기록](https://namu.wiki/w/the%20seed/%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8#toc)을 참고하여, 모방할 the seed 엔진의 버전을 지정한다(형식 주의! 4.4(X), "4.4"(X), 4.4.1(X), "4.4.1"(O) 문자열 x.y.z 형식으로). 예를 들어, "4.4.2"로 할 경우, v4.4.3에 추가된 쓰레드 주제/문서 변경 기능을 사용할 수 없고, "4.18.0"으로 할 경우 IPACL과 사용자 차단 기능이 비활성화되고 ACLGroup가 활성화되며 ACL에서 이름공간ACL 실행 action를 사용할 수 있다. 60 | - `replicate_theseed_license`: (기본값 false) 라이선스 페이지를 더시드 엔진처럼 띄운다. 가급적이면 쓰지 않는 것을 권장한다. 61 | - `namuwiki_exclusive`: (기본값 false) 나무위키 전용 기능(경고 ACL 그룹, 문서 이전 판 경고 등)을 활성화한다. 62 | - `enable_captcha`: (기본값 false) 보안문자를 쓰게 한다. 63 | - `block_ip`: (기본값 []) 접속을 차단할 IP를 지정한다. CIDR는 지원하지 않는다. 64 | - `protect_owner`: (기본값 false) 소유자 보호 기능을 활성화한다. 65 | - `disable_multithreading`: (기본값 true) 멀티쓰레딩을 비활성화한다. 66 | - `custom_namespaces`: (기본값 []) 사용자 지정 이름공간 배열 67 | - `sessionhttp`: (기본값 false) true로 설정시, https접속시에만 로그인이 유지된다. 68 | - `mailhost`: (기본값 []) 이메일 호스트 설정. 69 | - `email`: (기본값 []) 이메일 주소. 70 | - `passwd`: (기본값 []) 이메일 주소의 비밀번호(gmail의 경우 앱 비밀번호). 71 | - `disable_file_server`: (기본값 false) 별도 파일 서버 없이도 파일 업로드가 가능하게 한다. 72 | - `max_file_size`: (기본값 2000000) 최대 파일 크기 (바이트 단위) 73 | 74 | ## 라이선스 75 | 자유롭게 쓰기 바란다. 76 | 77 | ## 더 시드와 다른 것들 78 | - 엔진에서 백엔드와 프론트엔드를 모두 처리한다. (오픈나무에서 영향 받음) 79 | - 밀리초 유닉스 시간을 사용한다. 80 | - /notify/thread 라우트가 제대로 되어있지 않다. 81 | 82 | -------------------------------------------------------------------------------- /routes/search.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/complete\/(.*)/, (req, res) => { 2 | // 초성검색은 나중에 3 | const query = req.params[0]; 4 | const doc = processTitle(query); 5 | curs.execute("select title, namespace from documents where lower(title) like ? || '%' and lower(namespace) = ? limit 10", [doc.title.toLowerCase(), doc.namespace.toLowerCase()]) 6 | .then(data => { 7 | var ret = []; 8 | for(var i of data) { 9 | ret.push(totitle(i.title, i.namespace) + ''); 10 | } 11 | return res.json(ret); 12 | }) 13 | .catch(e => { 14 | print(e.stack); 15 | return res.status(500).json([]); 16 | }); 17 | }); 18 | 19 | router.get(/^\/go\/(.*)/, (req, res) => { 20 | const query = req.params[0]; 21 | const doc = processTitle(query); 22 | curs.execute("select title, namespace from history where lower(title) = ? and lower(namespace) = ?", [doc.title.toLowerCase(), doc.namespace.toLowerCase()]) 23 | .then(data => { 24 | if(data.length) { 25 | const title = totitle(data[0].title, data[0].namespace); 26 | const idx = ranking.findIndex(item => item.keyword == query); 27 | if(idx > -1) 28 | ranking[idx].count++; 29 | else 30 | ranking.push({ keyword: query, count: 1 }); 31 | ranking = ranking.sort((l, r) => r.count - l.count).slice(0, 10); 32 | return res.redirect('/w/' + title); 33 | } 34 | else return res.redirect('/search/' + query); 35 | }) 36 | .catch(e => { 37 | return res.redirect('/search/' + query); 38 | }); 39 | }); 40 | 41 | router.get(/^\/search\/(.*)/, async(req, res) => { 42 | const query = req.params[0]; 43 | 44 | var content = ` 45 | 57 | `; 58 | 59 | var st = new Date().getTime() / 1000; 60 | 61 | if(!query.replace(/^(\s+)/, '').replace(/(\s+)$/, '')) { 62 | res.send(await render(req, '"' + query + '" 검색 결과', content, {}, _, _, 'search')); 63 | } 64 | 65 | http.request({ 66 | host: hostconfig.search_host, 67 | port: hostconfig.search_port, 68 | path: '/search/' + encodeURIComponent(query) + '?page=' + (req.query['page'] || '1'), 69 | }, async rr => { 70 | var d = ''; 71 | 72 | rr.on('data', function(chunk) { 73 | d += chunk; 74 | }); 75 | 76 | rr.on('end', async function() { 77 | const ret = JSON.parse(d); 78 | var reshtml = ''; 79 | reshtml += ` 80 |
81 | `; 82 | for(var item of ret.result) { 83 | var title = totitle(item.title, item.namespace) + ''; 84 | reshtml += ` 85 |
86 |

87 | 88 | 89 | ${html.escape(title)} 90 | 91 |

92 |
93 | ${item.content} 94 |
95 |
96 | `; 97 | } 98 | reshtml += ` 99 | 114 |
115 | `; 116 | var et = new Date().getTime() / 1000; 117 | content = content + ` 118 |
전체 ${ret.total} 건 / 처리 시간 ${(et - st).toFixed(3).replace(/([0]+)$/, '')}초
119 | ` + reshtml; 120 | res.send(await render(req, '"' + query + '" 검색 결과', content, {}, _, _, 'search')); 121 | }); 122 | }).on('error', async e => { 123 | res.send(await showError(req, 'searchd_fail')); 124 | }).end(); 125 | }); 126 | 127 | if(hostconfig.namuwiki_exclusive) router.get(/^\/api\/ranking$/, (req, res) => { 128 | res.json({ 129 | keywords: ranking.sort((l, r) => r.count - l.count).map(item => ({ keyword: item.keyword })).slice(0, 10) 130 | }); 131 | }); -------------------------------------------------------------------------------- /routes/grant.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/admin\/grant$/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | var username = req.query['username']; 4 | if(!hasperm(req, 'grant')) return res.send(await showError(req, 'permission')); 5 | 6 | var error = null; 7 | var content = ` 8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 | `; 17 | if(username === undefined) return res.send(await render(req, '권한 부여', content, {}, _, error, 'grant')); 18 | if(!username) return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'validator_required', tag: 'username' })) + content, {}, _, error, 'grant')); 19 | var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]); 20 | if(!data.length) 21 | return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'invalid_username' })) + content, {}, _, error, 'grant')); 22 | username = data[0].username; 23 | if((hostconfig.owners || []).includes(username) && hostconfig.protect_owners && username != ip_check(req)) 24 | return res.send(await showError(req, 'permission')); 25 | 26 | var chkbxs = ''; 27 | for(var prm of perms) { 28 | // if(!getperm('developer', ip_check(req), 1) && 'developer' == (prm)) continue; 29 | if(ver('4.20.0') && prm == 'no_force_recaptcha') prm = 'no_force_captcha'; 30 | chkbxs += ` 31 | ${prm}
32 | `; 33 | } 34 | 35 | content += ` 36 |

사용자 ${html.escape(username)}

37 | 38 |
39 |
40 | ${chkbxs} 41 |
42 | 43 | 44 |
45 | `; 46 | 47 | if(req.method == 'POST') { 48 | if(!username) return res.send(await showError(req, 'invalid_username')); 49 | var data = await curs.execute("select username from users where username = ?", [username]); 50 | if(!data.length) return res.send(await showError(req, 'invalid_username')); 51 | if((hostconfig.owners || []).includes(username) && hostconfig.protect_owners && username != ip_check(req)) 52 | return res.send(await showError(req, 'permission')); 53 | 54 | var prmval = req.body['permissions']; 55 | if(!prmval || !prmval.find) prmval = [prmval]; 56 | prmval = prmval.map(item => ver('4.20.0') && item == 'no_force_captcha' ? 'no_force_recaptcha' : item); 57 | 58 | var logstring = ''; 59 | for(var prm of perms) { 60 | // if(!getperm('developer', ip_check(req), 1) && 'developer' == (prm)) continue; 61 | if(getperm(prm, username, 1) && (typeof(prmval.find(item => item == prm)) == 'undefined')) { 62 | logstring += '-' + (ver('4.20.0') && item == 'no_force_recaptcha' ? 'no_force_captcha' : prm) + ' '; 63 | if(permlist[username]) permlist[username].splice(permlist[username].findIndex(item => item == prm), 1); 64 | curs.execute("delete from perms where perm = ? and username = ?", [prm, username]); 65 | } else if(!getperm(prm, username, 1) && (typeof(prmval.find(item => item == prm)) != 'undefined')) { 66 | logstring += '+' + (ver('4.20.0') && item == 'no_force_recaptcha' ? 'no_force_captcha' : prm) + ' '; 67 | if(!permlist[username]) permlist[username] = [prm]; 68 | else permlist[username].push(prm); 69 | curs.execute("insert into perms (perm, username) values (?, ?)", [prm, username]); 70 | } 71 | } 72 | if(!logstring.length) 73 | return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'no_change' })) + content, {}, _, error, 'grant')); 74 | 75 | var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1'); 76 | if(data.length) logid = Number(data[0].logid) + 1; 77 | insert('block_history', { 78 | date: getTime(), 79 | type: 'grant', 80 | note: logstring, 81 | ismember: islogin(req) ? 'author' : 'ip', 82 | executer: ip_check(req), 83 | target: username, 84 | logid, 85 | }); 86 | 87 | return res.redirect('/admin/grant?username=' + encodeURIComponent(username)); 88 | } 89 | 90 | res.send(await render(req, '권한 부여', content, {}, _, _, 'grant')); 91 | }); 92 | -------------------------------------------------------------------------------- /backend/unfinished/move.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/move\/(.*)/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | 4 | const title = req.params[0]; 5 | const doc = processTitle(title); 6 | 7 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2); 8 | if(aclmsg) return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 9 | 10 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'move', 1); 11 | if(aclmsg) return res.send(await showError(req, { code: 'permission_move', msg: aclmsg })); 12 | 13 | const o_o = await curs.execute("select title from history where title = ? and namespace = ?", [doc.title, doc.namespace]); 14 | if(!o_o.length) return res.send(await showError(req, 'document_not_found')); 15 | 16 | // 원래 이랬나...? 17 | var content = ` 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 | ${ver('4.2.4') ? ` 30 |
31 |
32 | 33 |
34 | ` : '' 35 | } 36 | 37 |
38 | 39 |
40 |
41 | `; 42 | 43 | var error = null; 44 | 45 | if(req.method == 'POST') do { 46 | if(doc.namespace == '사용자') 47 | if((ver('4.11.0') && !doc.title.includes('/')) || !ver('4.11.0')) { 48 | content = (error = err('alert', 'disable_user_document')) + content; 49 | break; 50 | } 51 | 52 | var doccontent = ''; 53 | const o_o = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 54 | if(o_o.length) { 55 | doccontent = o_o[0].content; 56 | } 57 | 58 | const _recentRev = await curs.execute("select content, rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 59 | const recentRev = _recentRev[0]; 60 | 61 | if(!req.body['title']) { 62 | content = (error = err('alert', { code: 'validator_required', tag: 'title' })) + content; 63 | break; 64 | } 65 | 66 | const newdoc = processTitle(req.body['title']); 67 | 68 | var aclmsg = await getacl(req, newdoc.title, newdoc.namespace, 'read', 1); 69 | if(aclmsg) { 70 | return res.send(await showError(req, { code: 'permission_read', msg: aclmsg })); 71 | } 72 | 73 | var aclmsg = await getacl(req, newdoc.title, newdoc.namespace, 'edit', 2); 74 | if(aclmsg) { 75 | return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 76 | } 77 | 78 | if(req.body['mode'] == 'swap') { 79 | return res.send(await showError(req, 'feature_not_implemented')); 80 | } else { 81 | const d_d = await curs.execute("select rev from history where title = ? and namespace = ?", [newdoc.title, newdoc.namespace]); 82 | if(d_d.length) { 83 | return res.send(await showError(req, '문서가 이미 존재합니다.', 1)); 84 | } 85 | 86 | await curs.execute("update documents set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 87 | await curs.execute("update acl set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 88 | curs.execute("update threads set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 89 | curs.execute("update edit_requests set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 90 | curs.execute("update history set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 91 | } 92 | 93 | curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \ 94 | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ 95 | newdoc.title, newdoc.namespace, doccontent, String(Number(recentRev.rev) + 1), ip_check(req), getTime(), '0', req.body['log'] || '', '0', '-1', islogin(req) ? 'author' : 'ip', 'move', doc.title + '\n' + newdoc.title 96 | ]); 97 | curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]); 98 | return res.redirect('/w/' + encodeURIComponent(newdoc + '')); 99 | } while(0); 100 | 101 | res.send(await render(req, doc + ' (이동)', content, { 102 | document: doc, 103 | }, '', error, 'move')); 104 | }); -------------------------------------------------------------------------------- /routes/block_history.js: -------------------------------------------------------------------------------- 1 | function parses(s) { 2 | s = Number(s); 3 | var ret = ''; 4 | if(s && s / 604800 >= 1) (ret += parseInt(s / 604800) + '주 '), s = s % 604800; 5 | if(s && s / 86400 >= 1) (ret += parseInt(s / 86400) + '일 '), s = s % 86400; 6 | if(s && s / 3600 >= 1) (ret += parseInt(s / 3600) + '시간 '), s = s % 3600; 7 | if(s && s / 60 >= 1) (ret += parseInt(s / 60) + '분 '), s = s % 60; 8 | if(s && s / 1 >= 1) (ret += parseInt(s / 1) + '초 '), s = s % 1; 9 | 10 | return ret.replace(/\s$/, ''); 11 | } 12 | 13 | router.get(/^\/BlockHistory$/, async(req, res) => { 14 | var pa = []; 15 | var qq = " where '1' = '1' "; 16 | if(req.query['target'] && req.query['query']) { 17 | const com = req.query['query'].startsWith('"') && req.query['query'].endsWith('"'); 18 | const query = com ? req.query['query'].replace(/^\"/, '').replace(/\"$/, '') : req.query['query']; 19 | if(req.query['target'] == 'author') { 20 | qq = 'where executer ' + (com ? ' = ? ' : "like '%' || ? || '%' "); 21 | pa = [query]; 22 | } else { 23 | qq = 'where note ' + (com ? ' = ? ' : "like '%' || ? || '%' ") + ' or target ' + (com ? ' = ? ' : "like '%' || ? || '%' "); 24 | pa = [query, query]; 25 | } 26 | } 27 | var total = (await curs.execute("select count(logid) from block_history"))[0]['count(logid)']; 28 | 29 | const from = req.query['from']; 30 | const until = req.query['until']; 31 | var data; 32 | if(from) { 33 | data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " + 34 | qq + " and (cast(logid as integer) <= ? AND cast(logid as integer) > ?) order by cast(date as integer) desc limit 100", 35 | pa.concat([Number(from), Number(from) - 100])); 36 | } else if(until) { 37 | data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " + 38 | qq + " and (cast(logid as integer) >= ? AND cast(logid as integer) < ?) order by cast(date as integer) desc limit 100", 39 | pa.concat([Number(until), Number(until) + 100])); 40 | } else { 41 | data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " + 42 | qq + " order by cast(date as integer) desc limit 100", 43 | pa); 44 | } 45 | 46 | try { 47 | var navbtns = navbtn(total, data[data.length-1].logid, data[0].logid, '/BlockHistory'); 48 | } catch(e) { 49 | var navbtns = navbtn(0, 0, 0, 0); 50 | } 51 | var content = ` 52 |
53 | 57 | 58 | 59 | 60 |
61 | 62 | ${navbtns} 63 | 64 |
    65 | `; 66 | 67 | for(var item of data) { 68 | if(['aclgroup_add', 'aclgroup_remove'].includes(item.type) && !ver('4.18.0')) continue; 69 | 70 | content += ` 71 |
  • ${generateTime(toDate(item.date), timeFormat)} ${ip_pas(item.executer, item.ismember, 0, 1)} 사용자가 ${item.target} (${ 72 | item.type == 'aclgroup_add' 73 | ? `${item.aclgroup} ACL 그룹에 추가` 74 | : ( 75 | item.type == 'aclgroup_remove' 76 | ? `${item.aclgroup} ACL 그룹에서 제거` 77 | : ( 78 | item.type == 'ipacl_add' 79 | ? `IP 주소 차단` 80 | : ( 81 | item.type == 'ipacl_remove' 82 | ? `IP 주소 차단 해제` 83 | : ( 84 | item.type == 'login_history' 85 | ? `사용자 로그인 기록 조회` 86 | : ( 87 | item.type == 'suspend_account' && item.duration != '-1' 88 | ? `사용자 차단` 89 | : ( 90 | item.type == 'suspend_account' && item.duration == '-1' 91 | ? `사용자 차단 해제` 92 | : ( 93 | item.type == 'grant' 94 | ? `사용자 권한 설정` 95 | : '' 96 | ))))))) 97 | }) ${item.type == 'aclgroup_add' || item.type == 'aclgroup_remove' ? `#${item.id}` : ''} ${ 98 | item.type == 'aclgroup_add' || item.type == 'ipacl_add' || (item.type == 'suspend_account' && item.duration != '-1') 99 | ? (ver('4.0.20') ? `(${item.duration == '0' ? '영구적으로' : `${parses(item.duration)} 동안`})` : `(${item.duration} 동안)`) 100 | : '' 101 | } ${ 102 | item.type == 'aclgroup_add' || item.type == 'aclgroup_remove' || item.type == 'ipacl_add' || item.type == 'suspend_account' || item.type == 'grant' 103 | ? `(${item.note})` 104 | : '' 105 | }
  • 106 | `; 107 | } 108 | 109 | content += ` 110 |
111 | 112 | ${navbtns} 113 | `; 114 | 115 | return res.send(await render(req, '차단 내역', content, {}, _, _, 'block_history')); 116 | }); 117 | -------------------------------------------------------------------------------- /backend/unfinished/block_history.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/BlockHistory$/, async(req, res) => { 2 | var pa = []; 3 | var qq = " where '1' = '1' "; 4 | if(req.query['target'] && req.query['query']) { 5 | const com = req.query['query'].startsWith('"') && req.query['query'].endsWith('"'); 6 | const query = com ? req.query['query'].replace(/^\"/, '').replace(/\"$/, '') : req.query['query']; 7 | if(req.query['target'] == 'author') { 8 | qq = 'where executer' + (com ? ' = ? ' : "like '%' || ? || '%' "); 9 | pa = [query]; 10 | } else { 11 | qq = 'where note ' + (com ? ' = ? ' : "like '%' || ? || '%' ") + ' or target ' + (com ? ' = ? ' : "like '%' || ? || '%' "); 12 | pa = [query, query]; 13 | } 14 | } 15 | var total = (await curs.execute("select count(logid) from block_history"))[0]['count(logid)']; 16 | 17 | const from = req.query['from']; 18 | const until = req.query['until']; 19 | var data; 20 | if(from) { 21 | data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " + 22 | qq + " and (cast(logid as integer) <= ? AND cast(logid as integer) > ?) order by cast(date as integer) desc limit 100", 23 | pa.concat([Number(from), Number(from) - 100])); 24 | } else if(until) { 25 | data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " + 26 | qq + " and (cast(logid as integer) >= ? AND cast(logid as integer) < ?) order by cast(date as integer) desc limit 100", 27 | pa.concat([Number(until), Number(until) + 100])); 28 | } else { 29 | data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " + 30 | qq + " order by cast(date as integer) desc limit 100", 31 | pa); 32 | } 33 | 34 | try { 35 | var navbtns = navbtn(total, data[data.length-1].logid, data[0].logid, '/BlockHistory'); 36 | } catch(e) { 37 | var navbtns = navbtn(0, 0, 0, 0); 38 | } 39 | var content = ` 40 |
41 | 45 | 46 | 47 | 48 |
49 | 50 | ${navbtns} 51 | 52 |
    53 | `; 54 | 55 | function parses(s) { 56 | s = Number(s); 57 | var ret = ''; 58 | if(s && s / 604800 >= 1) (ret += parseInt(s / 604800) + '주 '), s = s % 604800; 59 | if(s && s / 86400 >= 1) (ret += parseInt(s / 86400) + '일 '), s = s % 86400; 60 | if(s && s / 3600 >= 1) (ret += parseInt(s / 3600) + '시간 '), s = s % 3600; 61 | if(s && s / 60 >= 1) (ret += parseInt(s / 60) + '분 '), s = s % 60; 62 | if(s && s / 1 >= 1) (ret += parseInt(s / 1) + '초 '), s = s % 1; 63 | 64 | return ret.replace(/\s$/, ''); 65 | } 66 | 67 | for(var item of data) { 68 | if(['aclgroup_add', 'aclgroup_remove'].includes(item.type) && !ver('4.18.0')) continue; 69 | 70 | content += ` 71 |
  • ${generateTime(toDate(item.date), timeFormat)} ${ip_pas(item.executer, item.ismember)} 사용자가 ${item.target} (${ 72 | item.type == 'aclgroup_add' 73 | ? `${item.aclgroup} ACL 그룹에 추가` 74 | : ( 75 | item.type == 'aclgroup_remove' 76 | ? `${item.aclgroup} ACL 그룹에서 제거` 77 | : ( 78 | item.type == 'ipacl_add' 79 | ? `IP 주소 차단` 80 | : ( 81 | item.type == 'ipacl_remove' 82 | ? `IP 주소 차단 해제` 83 | : ( 84 | item.type == 'login_history' 85 | ? `사용자 로그인 기록 조회` 86 | : ( 87 | item.type == 'suspend_account' && item.duration != '-1' 88 | ? `사용자 차단` 89 | : ( 90 | item.type == 'suspend_account' && item.duration == '-1' 91 | ? `사용자 차단 해제` 92 | : ( 93 | item.type == 'grant' 94 | ? `사용자 권한 설정` 95 | : '' 96 | ))))))) 97 | }) ${item.type == 'aclgroup_add' || item.type == 'aclgroup_remove' ? `#${item.id}` : ''} ${ 98 | item.type == 'aclgroup_add' || item.type == 'ipacl_add' || (item.type == 'suspend_account' && item.duration != '-1') 99 | ? (major == 4 && ver('4.0.20') ? `(${item.duration == '0' ? '영구적으로' : `${parses(item.duration)} 동안`})` : `${item.duration} 동안`) 100 | : '' 101 | } ${ 102 | item.type == 'aclgroup_add' || item.type == 'aclgroup_remove' || item.type == 'ipacl_add' || item.type == 'suspend_account' || item.type == 'grant' 103 | ? `(${item.note})` 104 | : '' 105 | }
  • 106 | `; 107 | } 108 | 109 | content += ` 110 |
111 | 112 | ${navbtns} 113 | `; 114 | 115 | return res.send(await render(req, '차단 내역', content, {}, _, _, 'block_history')); 116 | }); -------------------------------------------------------------------------------- /routes/move.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/move\/(.*)/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | 4 | const title = req.params[0]; 5 | const doc = processTitle(title); 6 | 7 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2, 1); 8 | if(aclmsg) return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 9 | 10 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'move', 1); 11 | if(aclmsg) return res.send(await showError(req, { code: 'permission_move', msg: aclmsg })); 12 | 13 | const o_o = await curs.execute("select title from history where title = ? and namespace = ?", [doc.title, doc.namespace]); 14 | if(!o_o.length) return res.send(await showError(req, 'document_not_found')); 15 | 16 | // 원래 이랬나...? 17 | var content = ` 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 | ${ver('4.2.4') ? ` 30 |
31 |
32 | 33 |
34 | ` : '' 35 | } 36 | 37 |
38 | 39 |
40 |
41 | `; 42 | 43 | var error = null; 44 | 45 | if(req.method == 'POST') do { 46 | if(doc.namespace == '사용자') 47 | if((ver('4.11.0') && !doc.title.includes('/')) || !ver('4.11.0')) { 48 | content = (error = err('alert', 'disable_user_document')) + content; 49 | break; 50 | } 51 | 52 | var doccontent = ''; 53 | const o_o = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 54 | if(o_o.length) { 55 | doccontent = o_o[0].content; 56 | } 57 | 58 | const _recentRev = await curs.execute("select content, rev from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [doc.title, doc.namespace]); 59 | const recentRev = _recentRev[0]; 60 | 61 | if(!req.body['title']) { 62 | content = (error = err('alert', { code: 'validator_required', tag: 'title' })) + content; 63 | break; 64 | } 65 | 66 | const newdoc = processTitle(req.body['title']); 67 | 68 | var aclmsg = await getacl(req, newdoc.title, newdoc.namespace, 'read', 1); 69 | if(aclmsg) { 70 | return res.send(await showError(req, { code: 'permission_read', msg: aclmsg })); 71 | } 72 | 73 | var aclmsg = await getacl(req, newdoc.title, newdoc.namespace, 'edit', 2); 74 | if(aclmsg) { 75 | return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg })); 76 | } 77 | 78 | if(req.body['mode'] == 'swap') { 79 | return res.send(await showError(req, 'feature_not_implemented')); 80 | } else { 81 | const d_d = await curs.execute("select rev from history where title = ? and namespace = ?", [newdoc.title, newdoc.namespace]); 82 | if(d_d.length) { 83 | return res.send(await showError(req, '문서가 이미 존재합니다.', 1)); 84 | } 85 | 86 | await curs.execute("update documents set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 87 | await curs.execute("update acl set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 88 | curs.execute("update threads set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 89 | curs.execute("update edit_requests set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 90 | curs.execute("update history set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 91 | 92 | await curs.execute("delete from files where title = ? and namespace = ?", [newdoc.title, newdoc.namespace]); 93 | await curs.execute("update files set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]); 94 | } 95 | 96 | curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \ 97 | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ 98 | newdoc.title, newdoc.namespace, doccontent, String(Number(recentRev.rev) + 1), ip_check(req), getTime(), '0', req.body['log'] || '', '0', '-1', islogin(req) ? 'author' : 'ip', 'move', doc.title + '\n' + newdoc.title 99 | ]); 100 | curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]); 101 | return res.redirect('/w/' + encodeURIComponent(newdoc + '')); 102 | } while(0); 103 | 104 | res.send(await render(req, doc + ' (이동)', content, { 105 | document: doc, 106 | }, '', error, 'move')); 107 | }); -------------------------------------------------------------------------------- /routes/backlink.js: -------------------------------------------------------------------------------- 1 | if(ver('4.14.0') && hostconfig.namuwiki_exclusive) router.get(/^\/xref\/(.*)/, (req, res) => { 2 | const title = req.params[0]; 3 | res.redirect('/backlink/' + encodeURIComponent(title) + '?flag=' + encodeURIComponent(req.query['flag'] || '0') + '&namespace=' + encodeURIComponent(req.query['namespace'] || '문서')); 4 | }); 5 | 6 | router.get(ver('4.14.0') ? /^\/backlink\/(.*)/ : /^\/xref\/(.*)/, async (req, res) => { 7 | const title = req.params[0]; 8 | const doc = processTitle(title); 9 | const flag = req.query['flag'] || '0'; 10 | const ns = req.query['namespace'] || '문서'; 11 | const type = ( 12 | flag == '1' ? ( 13 | 'link' 14 | ) : ( 15 | flag == '2' ? ( 16 | 'file' 17 | ) : ( 18 | flag == '4' ? ( 19 | 'include' 20 | ) : flag == '8' ? ( 21 | 'redirect' 22 | ) : 'all' 23 | ) 24 | ) 25 | ); 26 | 27 | var sa = '', sd = []; 28 | if(req.query['from']) { 29 | sa = ' and title >= ? order by title asc '; 30 | sd.push(req.query['from']); 31 | } else if(req.query['until']) { 32 | sa = ' and title <= ? order by title desc '; 33 | sd.push(req.query['until']); 34 | } else { 35 | sa = ' order by title asc '; 36 | } 37 | const fd = await curs.execute("select title from backlink where not type = 'category' and link = ? and linkns = ? " + (flag != '0' ? " and type = ?" : '') + " order by title asc limit 1", [doc.title, doc.namespace].concat(flag != '0' ? [type] : [])); 38 | const ld = await curs.execute("select title from backlink where not type = 'category' and link = ? and linkns = ? " + (flag != '0' ? " and type = ?" : '') + " order by title desc limit 1", [doc.title, doc.namespace].concat(flag != '0' ? [type] : [])); 39 | const dbdata = await curs.execute("select title, namespace, type from backlink where not type = 'category' and link = ? and linkns = ? " + (flag != '0' ? " and type = ?" : '') + sa + " limit 50", [doc.title, doc.namespace].concat(flag != '0' ? [type] : []).concat(sd)); 40 | 41 | try { 42 | var navbtns = navbtnss(fd[0].title, ld[0].title, dbdata[0].title, dbdata[dbdata.length-1].title, (ver('4.14.0') ? '/backlink/' : '/xref/') + encodeURIComponent(title)); 43 | } catch(e) { 44 | var navbtns = navbtn(0, 0, 0, 0); 45 | } 46 | 47 | const _nslist = dbdata.map(item => item.namespace); 48 | const nslist = fetchNamespaces().filter(item => _nslist.includes(item)); 49 | const counts = {}; 50 | var nsopt = ''; 51 | for(var item of nslist) { 52 | nsopt += ``; 53 | } 54 | const data = dbdata.filter(item => item.namespace == ns); 55 | 56 | var content = ` 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 71 |
72 | 73 |
74 | 75 |
76 |
77 |
78 | `; 79 | 80 | var indexes = {}; 81 | const hj = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']; 82 | const ha = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하', String.fromCharCode(55204)]; 83 | for(var item of data) { 84 | if(!item) continue; 85 | var chk = 0; 86 | for(var i=0; i= ha[i].charCodeAt(0) && fchr < ha[i+1].charCodeAt(0))) { 90 | if(!indexes[hj[i]]) indexes[hj[i]] = []; 91 | indexes[hj[i]].push(item); 92 | chk = 1; 93 | break; 94 | } 95 | } if(!chk) { 96 | if(!indexes[item.title[0].toUpperCase()]) indexes[item.title[0].toUpperCase()] = []; 97 | indexes[item.title[0].toUpperCase()].push(item); 98 | } 99 | } 100 | 101 | var listc = ' 6 ? ' class=wiki-category-container' : '') + '>'; 102 | var list = ''; 103 | for(var idx of Object.keys(indexes).sort()) { 104 | list += ` 105 |
106 |

${html.escape(idx)}

107 |
'; 116 | } listc += list + ''; 117 | 118 | content += ` 119 | ${navbtns} 120 | ${list ? listc : '
해당 문서의 역링크가 존재하지 않습니다.
'} 121 | ${navbtns} 122 | `; 123 | 124 | res.send(await render(req, title + '의 역링크', content, { 125 | document: doc, 126 | }, _, _, 'xref')); 127 | }); -------------------------------------------------------------------------------- /backend/unfinished/backlink.js: -------------------------------------------------------------------------------- 1 | if(ver('4.14.0') && hostconfig.namuwiki_exclusive) router.get(/^\/xref\/(.*)/, (req, res) => { 2 | const title = req.params[0]; 3 | res.redirect('/backlink/' + encodeURIComponent(title) + '?flag=' + encodeURIComponent(req.query['flag'] || '0') + '&namespace=' + encodeURIComponent(req.query['namespace'] || '문서')); 4 | }); 5 | 6 | router.get(ver('4.14.0') ? /^\/backlink\/(.*)/ : /^\/xref\/(.*)/, async (req, res) => { 7 | const title = req.params[0]; 8 | const doc = processTitle(title); 9 | const flag = req.query['flag'] || '0'; 10 | const ns = req.query['namespace'] || '문서'; 11 | const type = ( 12 | flag == '1' ? ( 13 | 'link' 14 | ) : ( 15 | flag == '2' ? ( 16 | 'file' 17 | ) : ( 18 | flag == '4' ? ( 19 | 'include' 20 | ) : flag == '8' ? ( 21 | 'redirect' 22 | ) : 'all' 23 | ) 24 | ) 25 | ); 26 | 27 | var sa = '', sd = []; 28 | if(req.query['from']) { 29 | sa = ' and title >= ? order by title asc '; 30 | sd.push(req.query['from']); 31 | } else if(req.query['until']) { 32 | sa = ' and title <= ? order by title desc '; 33 | sd.push(req.query['until']); 34 | } else { 35 | sa = ' order by title asc '; 36 | } 37 | const fd = await curs.execute("select title from backlink where not type = 'category' and link = ? and linkns = ? " + (flag != '0' ? " and type = ?" : '') + " order by title asc limit 1", [doc.title, doc.namespace].concat(flag != '0' ? [type] : [])); 38 | const ld = await curs.execute("select title from backlink where not type = 'category' and link = ? and linkns = ? " + (flag != '0' ? " and type = ?" : '') + " order by title desc limit 1", [doc.title, doc.namespace].concat(flag != '0' ? [type] : [])); 39 | const dbdata = await curs.execute("select title, namespace, type from backlink where not type = 'category' and link = ? and linkns = ? " + (flag != '0' ? " and type = ?" : '') + sa + " limit 50", [doc.title, doc.namespace].concat(flag != '0' ? [type] : []).concat(sd)); 40 | 41 | try { 42 | var navbtns = navbtnss(fd[0].title, ld[0].title, dbdata[0].title, dbdata[dbdata.length-1].title, (ver('4.14.0') ? '/backlink/' : '/xref/') + encodeURIComponent(title)); 43 | } catch(e) { 44 | var navbtns = navbtn(0, 0, 0, 0); 45 | } 46 | 47 | const _nslist = dbdata.map(item => item.namespace); 48 | const nslist = fetchNamespaces().filter(item => _nslist.includes(item)); 49 | const counts = {}; 50 | var nsopt = ''; 51 | for(var item of nslist) { 52 | nsopt += ``; 53 | } 54 | const data = dbdata.filter(item => item.namespace == ns); 55 | 56 | var content = ` 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 71 |
72 | 73 |
74 | 75 |
76 |
77 |
78 | `; 79 | 80 | var indexes = {}; 81 | const hj = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']; 82 | const ha = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하', String.fromCharCode(55204)]; 83 | for(var item of data) { 84 | if(!item) continue; 85 | var chk = 0; 86 | for(var i=0; i= ha[i].charCodeAt(0) && fchr < ha[i+1].charCodeAt(0))) { 90 | if(!indexes[hj[i]]) indexes[hj[i]] = []; 91 | indexes[hj[i]].push(item); 92 | chk = 1; 93 | break; 94 | } 95 | } if(!chk) { 96 | if(!indexes[item.title[0].toUpperCase()]) indexes[item.title[0].toUpperCase()] = []; 97 | indexes[item.title[0].toUpperCase()].push(item); 98 | } 99 | } 100 | 101 | var listc = ' 6 ? ' class=wiki-category-container' : '') + '>'; 102 | var list = ''; 103 | for(var idx of Object.keys(indexes).sort()) { 104 | list += ` 105 |
106 |

${html.escape(idx)}

107 |
'; 116 | } listc += list + ''; 117 | 118 | content += ` 119 | ${navbtns} 120 | ${list ? listc : '
해당 문서의 역링크가 존재하지 않습니다.
'} 121 | ${navbtns} 122 | `; 123 | 124 | res.send(await render(req, title + '의 역링크', content, { 125 | document: doc, 126 | }, _, _, 'xref')); 127 | }); -------------------------------------------------------------------------------- /backend/unfinished/license.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/License$/, async(req, res) => { 2 | var licepage = ` 3 |

imitated-seed

4 |

(the seed v${major}.${minor}.${revision})

5 | `; 6 | 7 | if(hostconfig.replicate_theseed_license) { 8 | licepage = ''; 9 | if(ver('4.11.1')) { 10 | licepage += `

the seed

v${major}.${minor}.${revision}

`; 11 | } else { 12 | licepage += `

the seed (v${major}.${minor}.${revision})

`; 13 | } 14 | licepage += ` 15 |

Copyright theseed.io all rights reserved.

16 | 17 |

Contributors

18 |
    19 | ${ 20 | ver('4.13.0') ? ` 21 |
  • namu@theseed.io (backend & frontend)
  • 22 |
  • PPPP@theseed.io (old frontend)
  • 23 |
  • kasio@theseed.io (old render)
  • 24 | ` : ` 25 |
  • namu@theseed.io (backend)
  • 26 |
  • PPPP@theseed.io (frontend)
  • 27 |
  • kasio@theseed.io (old render)
  • 28 | ` 29 | } 30 |
31 | 32 |

Open source license

33 |
    34 |
  • 35 | KaTex
    36 | Author : Khan Academy
    37 | KaTeX is licensed under the MIT license. 38 |
  • 39 |
  • 40 | Swig
    41 | Author : Paul Armstrong
    42 | Swig is licensed under the MIT license. 43 |
  • 44 | 45 | ${ver('4.13.0') ? ` 46 |
  • /*!
     47 |  * nano-assign v1.0.1
     48 |  * (c) 2018-present egoist <0x142857@gmail.com>
     49 |  * Released under the MIT License.
     50 |  */
     51 | /*!
     52 |  * Vue.js v2.6.10
     53 |  * (c) 2014-2019 Evan You
     54 |  * Released under the MIT License.
     55 |  */
     56 | 
     57 | /**!
     58 |  * @fileOverview Kickass library to create and place poppers near their reference elements.
     59 |  * @version 1.16.0
     60 |  * @license
     61 |  * Copyright (c) 2016 Federico Zivolo and contributors
     62 |  *
     63 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
     64 |  * of this software and associated documentation files (the "Software"), to deal
     65 |  * in the Software without restriction, including without limitation the rights
     66 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     67 |  * copies of the Software, and to permit persons to whom the Software is
     68 |  * furnished to do so, subject to the following conditions:
     69 |  *
     70 |  * The above copyright notice and this permission notice shall be included in all
     71 |  * copies or substantial portions of the Software.
     72 |  *
     73 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     74 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     75 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     76 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     77 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     78 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     79 |  * SOFTWARE.
     80 |  */
     81 | 
     82 | /**
     83 |  * vuex v3.1.2
     84 |  * (c) 2019 Evan You
     85 |  * @license MIT
     86 |  */
     87 | 
     88 | /**
     89 |  * vue-global-events v1.1.2
     90 |  * (c) 2019 Damian Dulisz <damian.dulisz@gmail.com>, Eduardo San Martin Morote <posva13@gmail.com>
     91 |  * @license MIT
     92 |  */
     93 | 
     94 | /*!
     95 |  * Determine if an object is a Buffer
     96 |  *
     97 |  * @author   Feross Aboukhadijeh <https://feross.org>
     98 |  * @license  MIT
     99 |  */
    100 | 
    101 | /**!
    102 |  * Sortable 1.10.1
    103 |  * @author	RubaXa   <trash@rubaxa.org>
    104 |  * @author	owenm    <owen23355@gmail.com>
    105 |  * @license MIT
    106 |  */
    107 | /*!---------------------------------------------------------------------------------------------
    108 |  *  Copyright (C) David Owens II, owensd.io. All rights reserved.
    109 |  *--------------------------------------------------------------------------------------------*/
    110 | /*! *****************************************************************************
    111 | Copyright (c) Microsoft Corporation. All rights reserved.
    112 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use
    113 | this file except in compliance with the License. You may obtain a copy of the
    114 | License at http://www.apache.org/licenses/LICENSE-2.0
    115 | 
    116 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    117 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
    118 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
    119 | MERCHANTABLITY OR NON-INFRINGEMENT.
    120 | 
    121 | See the Apache Version 2.0 License for specific language governing permissions
    122 | and limitations under the License.
    123 | ***************************************************************************** */
    124 | 
  • 125 | ` : ''} 126 |
127 | `; 128 | } 129 | 130 | if(!ver('4.13.0')) { 131 | licepage += await readFile('./skins/' + getSkin(req) + '/license.html') 132 | } 133 | 134 | return res.send(await render(req, '라이선스', ` 135 |
136 | ${licepage} 137 | ` + '
', {}, _, _, 'license')); 138 | }); -------------------------------------------------------------------------------- /routes/license.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/License$/, async(req, res) => { 2 | var licepage = ` 3 |

imitated-seed

4 |

(the seed v${version.major}.${version.minor}.${version.revision})

5 | `; 6 | 7 | if(hostconfig.replicate_theseed_license) { 8 | licepage = ''; 9 | if(ver('4.11.1')) { 10 | licepage += `

the seed

v${version.major}.${version.minor}.${version.revision}

`; 11 | } else { 12 | licepage += `

the seed (v${version.major}.${version.minor}.${version.revision})

`; 13 | } 14 | licepage += ` 15 |

Copyright theseed.io all rights reserved.

16 | 17 |

Contributors

18 |
    19 | ${ 20 | ver('4.13.0') ? ` 21 |
  • namu@theseed.io (backend & frontend)
  • 22 |
  • PPPP@theseed.io (old frontend)
  • 23 |
  • kasio@theseed.io (old render)
  • 24 | ` : ` 25 |
  • namu@theseed.io (backend)
  • 26 |
  • PPPP@theseed.io (frontend)
  • 27 |
  • kasio@theseed.io (old render)
  • 28 | ` 29 | } 30 |
31 | 32 |

Open source license

33 |
    34 |
  • 35 | KaTex
    36 | Author : Khan Academy
    37 | KaTeX is licensed under the MIT license. 38 |
  • 39 |
  • 40 | Swig
    41 | Author : Paul Armstrong
    42 | Swig is licensed under the MIT license. 43 |
  • 44 | 45 | ${ver('4.13.0') ? ` 46 |
  • /*!
     47 |  * nano-assign v1.0.1
     48 |  * (c) 2018-present egoist <0x142857@gmail.com>
     49 |  * Released under the MIT License.
     50 |  */
     51 | /*!
     52 |  * Vue.js v2.6.10
     53 |  * (c) 2014-2019 Evan You
     54 |  * Released under the MIT License.
     55 |  */
     56 | 
     57 | /**!
     58 |  * @fileOverview Kickass library to create and place poppers near their reference elements.
     59 |  * @version 1.16.0
     60 |  * @license
     61 |  * Copyright (c) 2016 Federico Zivolo and contributors
     62 |  *
     63 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
     64 |  * of this software and associated documentation files (the "Software"), to deal
     65 |  * in the Software without restriction, including without limitation the rights
     66 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     67 |  * copies of the Software, and to permit persons to whom the Software is
     68 |  * furnished to do so, subject to the following conditions:
     69 |  *
     70 |  * The above copyright notice and this permission notice shall be included in all
     71 |  * copies or substantial portions of the Software.
     72 |  *
     73 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     74 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     75 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     76 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     77 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     78 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     79 |  * SOFTWARE.
     80 |  */
     81 | 
     82 | /**
     83 |  * vuex v3.1.2
     84 |  * (c) 2019 Evan You
     85 |  * @license MIT
     86 |  */
     87 | 
     88 | /**
     89 |  * vue-global-events v1.1.2
     90 |  * (c) 2019 Damian Dulisz <damian.dulisz@gmail.com>, Eduardo San Martin Morote <posva13@gmail.com>
     91 |  * @license MIT
     92 |  */
     93 | 
     94 | /*!
     95 |  * Determine if an object is a Buffer
     96 |  *
     97 |  * @author   Feross Aboukhadijeh <https://feross.org>
     98 |  * @license  MIT
     99 |  */
    100 | 
    101 | /**!
    102 |  * Sortable 1.10.1
    103 |  * @author	RubaXa   <trash@rubaxa.org>
    104 |  * @author	owenm    <owen23355@gmail.com>
    105 |  * @license MIT
    106 |  */
    107 | /*!---------------------------------------------------------------------------------------------
    108 |  *  Copyright (C) David Owens II, owensd.io. All rights reserved.
    109 |  *--------------------------------------------------------------------------------------------*/
    110 | /*! *****************************************************************************
    111 | Copyright (c) Microsoft Corporation. All rights reserved.
    112 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use
    113 | this file except in compliance with the License. You may obtain a copy of the
    114 | License at http://www.apache.org/licenses/LICENSE-2.0
    115 | 
    116 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    117 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
    118 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
    119 | MERCHANTABLITY OR NON-INFRINGEMENT.
    120 | 
    121 | See the Apache Version 2.0 License for specific language governing permissions
    122 | and limitations under the License.
    123 | ***************************************************************************** */
    124 | 
  • 125 | ` : ''} 126 |
127 | `; 128 | } 129 | 130 | if(!ver('4.13.0')) { 131 | licepage += await readFile('./skins/' + getSkin(req) + '/license.html') 132 | } 133 | 134 | return res.send(await render(req, '라이선스', ` 135 |
136 | ${licepage} 137 | ` + '
', {}, _, _, 'license')); 138 | }); 139 | -------------------------------------------------------------------------------- /routes/public_api.js: -------------------------------------------------------------------------------- 1 | if(ver('4.20.0')) { 2 | router.get(/^\/api\/edit\/(.*)$/, async(req, res) => { 3 | var auth = req.headers['authorization'] || ''; 4 | if(!auth.match(/^Bearer\s([a-zA-Z0-9\=\+\/]+)$/)) 5 | return res.status(403).json({ 6 | status: err('raw', 'permission'), 7 | }); 8 | auth = auth.match(/^Bearer\s([a-zA-Z0-9\=\+\/]+)$/)[1]; 9 | var dbdata = await curs.execute("select username from api_tokens where token = ?", [auth]); 10 | if(!dbdata.length) { 11 | return res.status(403).json({ 12 | status: err('raw', 'permission'), 13 | }); 14 | } 15 | const username = dbdata[0].username; 16 | if(!getperm('api_access', username)) 17 | return res.status(403).json({ 18 | status: err('raw', 'permission'), 19 | }); 20 | const title = req.params[0]; 21 | const doc = processTitle(title); 22 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 23 | if(aclmsg) return res.status(403).json({ 24 | status: err('raw', { code: 'permission_read', msg: aclmsg }), 25 | }); 26 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 1); 27 | if(aclmsg) return res.status(403).json({ 28 | status: err('raw', { code: 'permission_edit', msg: aclmsg }), 29 | }); 30 | var exists = true; 31 | var text = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 32 | if(!text[0]) text = '', exists = false; 33 | else text = text[0].content.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); 34 | const token = Buffer.from(rndval('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 48)).toString('base64'); 35 | res.json({ 36 | text, exists, token, 37 | }); 38 | apiTokens[username] = token; 39 | }); 40 | 41 | router.post(/^\/api\/edit\/(.*)$/, async(req, res) => { 42 | var auth = req.headers['authorization'] || ''; 43 | if(!auth.match(/^Bearer\s([a-zA-Z0-9\=\+\/]+)$/)) 44 | return res.status(403).json({ 45 | status: err('raw', 'permission'), 46 | }); 47 | auth = auth.match(/^Bearer\s([a-zA-Z0-9\=\+\/]+)$/)[1]; 48 | var dbdata = await curs.execute("select username from api_tokens where token = ?", [auth]); 49 | if(!dbdata.length) { 50 | return res.status(403).json({ 51 | status: err('raw', 'permission'), 52 | }); 53 | } 54 | const username = dbdata[0].username; 55 | if(!getperm('api_access', username)) 56 | return res.status(403).json({ 57 | status: err('raw', 'permission'), 58 | }); 59 | const title = req.params[0]; 60 | const doc = processTitle(title); 61 | 62 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1); 63 | if(aclmsg) return res.status(403).json({ 64 | status: err('raw', { code: 'permission_read', msg: aclmsg }), 65 | }); 66 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 1); 67 | if(aclmsg) return res.status(403).json({ 68 | status: err('raw', { code: 'permission_edit', msg: aclmsg }), 69 | }); 70 | 71 | if(!apiTokens[username] || !req.body['token'] || apiTokens[username] != req.body['token']) 72 | return res.status(400).json({ 73 | status: err('raw', 'invalid_token'), 74 | }); 75 | 76 | var original = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 77 | var ex = 1; 78 | if(!original[0]) ex = 0, original = ''; 79 | else original = original[0]['content']; 80 | var text = req.body['text'] || ''; 81 | text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); 82 | if(text.startsWith('#넘겨주기 ')) text = text.replace('#넘겨주기 ', '#redirect '); 83 | if(text.startsWith('#redirect ')) text = text.split('\n')[0] + '\n'; 84 | const rawChanges = text.length - original.length; 85 | const changes = (rawChanges > 0 ? '+' : '') + String(rawChanges); 86 | const log = req.body['log'] || ''; 87 | var baserev = 0; 88 | var data = await curs.execute("select rev from history where title = ? and namespace = ? order by CAST(rev AS INTEGER) desc limit 1", [doc.title, doc.namespace]); 89 | if(data.length) baserev = data[0].rev; 90 | const ismember = 'author'; 91 | var advance = 'normal'; 92 | var data = await curs.execute("select title from documents where title = ? and namespace = ?", [doc.title, doc.namespace]); 93 | if(!data.length) { 94 | if(['파일', '사용자'].includes(doc.namespace)) { 95 | if((ver('4.11.0') && !doc.title.includes('/')) || !ver('4.11.0')) { 96 | return res.status(400).json({ 97 | status: err('raw', { code: 'invalid_namespace' }), 98 | }); } } 99 | advance = 'create'; 100 | await curs.execute("insert into documents (title, namespace, content) values (?, ?, ?)", [doc.title, doc.namespace, text]); 101 | } else { 102 | await curs.execute("update documents set content = ? where title = ? and namespace = ?", [text, doc.title, doc.namespace]); 103 | curs.execute("update stars set lastedit = ? where title = ? and namespace = ?", [getTime(), doc.title, doc.namespace]); 104 | } 105 | 106 | curs.execute("update documents set time = ? where title = ? and namespace = ?", [getTime(), doc.title, doc.namespace]); 107 | curs.execute("insert into history (isapi, title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance) \ 108 | values ('1', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ 109 | doc.title, doc.namespace, text, String(Number(baserev) + 1), username, getTime(), changes, log, '0', '-1', ismember, advance 110 | ]); 111 | markdown(req, text, 0, doc + '', 'backlinkinit'); 112 | 113 | delete(apiTokens[username]); 114 | 115 | return res.json({ 116 | status: 'success', 117 | rev: Number(baserev) + 1, 118 | }); 119 | }); 120 | } -------------------------------------------------------------------------------- /routes/change_username.js: -------------------------------------------------------------------------------- 1 | if(hostconfig.allow_account_rename) router.all(/^\/member\/change_username$/, async(req, res, next) => { 2 | if(!['GET', 'POST'].includes(req.method)) return next(); 3 | if(!islogin(req)) return res.redirect('/member/login?redirect=%2Fmember%2Fdelete_account'); 4 | const username = ip_check(req); 5 | var error = false; 6 | 7 | var { password } = (await curs.execute("select password from users where username = ?", [username]))[0]; 8 | 9 | if(req.method == 'POST') { 10 | if(!req.body['new_username']) 11 | var nonewusername = 1; 12 | 13 | var data = await curs.execute("select username from users where lower(username) = ? COLLATE NOCASE", [req.body['new_username'].toLowerCase()]); 14 | if(data.length) 15 | var duplicate = 1; 16 | 17 | if(!hostconfig.no_username_format && (id.length < 3 || id.length > 32 || id.match(/(?:[^A-Za-z0-9_])/))) 18 | var invalidformat = 1; 19 | } 20 | 21 | var content = ` 22 |
23 | ${!error && req.method == 'POST' && nonewusername ? (error = true, alertBalloon(fetchErrorString('validator_required', 'new_username'), 'danger', true, 'fade in')) : ''} 24 | ${(hostconfig.owners || []).includes(username) ? `

수정 후 반드시 config.json의 <owners> 값을 바꿔 주세요.

` : ''} 25 |

이름을 바꾸면 다른 사람이 당신의 기존 이름으로 가입할 수 있습니다.

26 | 27 |
28 | 29 | 30 | ${!error && req.method == 'POST' && req.body['username'] != username ? (error = true, `

자신의 사용자 이름을 입력해주세요.

`) : ''} 31 |
32 | 33 |
34 | 35 | 36 | ${!error && req.method == 'POST' && sha3(req.body['password'] + '') != password ? (error = true, `

비밀번호를 확인해주세요.

`) : ''} 37 |
38 | 39 |
40 | 41 | 42 | ${!error && req.method == 'POST' && duplicate ? (error = true, `

사용자 이름이 이미 존재합니다.

`) : ''} 43 | ${!error && req.method == 'POST' && invalidformat ? (error = true, `

사용자 이름을 형식에 맞게 입력해주세요.

`) : ''} 44 |
45 | 46 |
47 | 취소 48 | 취소 49 | 50 | 취소 51 | 취소 52 | 취소 53 |
54 |
55 | `; 56 | 57 | if(req.method == 'POST' && !error) { 58 | var newusername = req.body['new_username']; 59 | await curs.execute("update users set username = ? where username = ?", [newusername, username]); 60 | await curs.execute("update perms set username = ? where username = ?", [newusername, username]); 61 | await curs.execute("update suspend_account set username = ? where username = ?", [newusername, username]); 62 | await curs.execute("update user_settings set username = ? where username = ?", [newusername, username]); 63 | await curs.execute("update acl set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 64 | await curs.execute("update classic_acl set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 65 | await curs.execute("update documents set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 66 | await curs.execute("update threads set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 67 | await curs.execute("update edit_requests set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 68 | await curs.execute("update history set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 69 | await curs.execute("update login_history set username = ? where username = ?", [newusername, username]); 70 | await curs.execute("update stars set username = ? where username = ?", [newusername, username]); 71 | await curs.execute("update useragents set username = ? where username = ?", [newusername, username]); 72 | await curs.execute("update history set username = ? where username = ? and ismember = 'author'", [newusername, username]); 73 | await curs.execute("update res set username = ? where username = ? and ismember = 'author'", [newusername, username]); 74 | await curs.execute("update res set hider = ? where hider = ?", [newusername, username]); 75 | await curs.execute("update block_history set executer = ? where executer = ? and ismember = 'author'", [newusername, username]); 76 | await curs.execute("update block_history set target = ? where target = ?", [newusername, username]); 77 | await curs.execute("update edit_requests set processor = ? where processor = ? and ismember = 'author'", [newusername, username]); 78 | await curs.execute("update edit_requests set username = ? where username = ? and ismember = 'author'", [newusername, username]); 79 | await curs.execute("update autologin_tokens set username = ? where username = ?", [newusername, username]); 80 | req.session.username = newusername; 81 | permlist[newusername] = permlist[username]; 82 | delete permlist[username]; 83 | userset[newusername] = userset[username]; 84 | delete userset[username]; 85 | return res.send(await render(req, '사용자 이름 변경', ` 86 |

${html.escape(newusername)}(으)로 이름을 변경하였습니다.

87 | `, {}, _, false, 'delete_account')); 88 | } 89 | 90 | return res.send(await render(req, '사용자 이름 변경', content, {}, _, error, 'delete_account')); 91 | }); -------------------------------------------------------------------------------- /backend/unfinished/change_username.js: -------------------------------------------------------------------------------- 1 | if(hostconfig.allow_account_rename) router.all(/^\/member\/change_username$/, async(req, res, next) => { 2 | if(!['GET', 'POST'].includes(req.method)) return next(); 3 | if(!islogin(req)) return res.redirect('/member/login?redirect=%2Fmember%2Fdelete_account'); 4 | const username = ip_check(req); 5 | var error = false; 6 | 7 | var { password } = (await curs.execute("select password from users where username = ?", [username]))[0]; 8 | 9 | if(req.method == 'POST') { 10 | if(!req.body['new_username']) 11 | var nonewusername = 1; 12 | 13 | var data = await curs.execute("select username from users where lower(username) = ? COLLATE NOCASE", [req.body['new_username'].toLowerCase()]); 14 | if(data.length) 15 | var duplicate = 1; 16 | 17 | if(!hostconfig.no_username_format && (id.length < 3 || id.length > 32 || id.match(/(?:[^A-Za-z0-9_])/))) 18 | var invalidformat = 1; 19 | } 20 | 21 | var content = ` 22 |
23 | ${!error && req.method == 'POST' && nonewusername ? (error = true, alertBalloon(fetchErrorString('validator_required', 'new_username'), 'danger', true, 'fade in')) : ''} 24 | ${(hostconfig.owners || []).includes(username) ? `

수정 후 반드시 config.json의 <owners> 값을 바꿔 주세요.

` : ''} 25 |

이름을 바꾸면 다른 사람이 당신의 기존 이름으로 가입할 수 있습니다.

26 | 27 |
28 | 29 | 30 | ${!error && req.method == 'POST' && req.body['username'] != username ? (error = true, `

자신의 사용자 이름을 입력해주세요.

`) : ''} 31 |
32 | 33 |
34 | 35 | 36 | ${!error && req.method == 'POST' && sha3(req.body['password'] + '') != password ? (error = true, `

비밀번호를 확인해주세요.

`) : ''} 37 |
38 | 39 |
40 | 41 | 42 | ${!error && req.method == 'POST' && duplicate ? (error = true, `

사용자 이름이 이미 존재합니다.

`) : ''} 43 | ${!error && req.method == 'POST' && invalidformat ? (error = true, `

사용자 이름을 형식에 맞게 입력해주세요.

`) : ''} 44 |
45 | 46 |
47 | 취소 48 | 취소 49 | 50 | 취소 51 | 취소 52 | 취소 53 |
54 |
55 | `; 56 | 57 | if(req.method == 'POST' && !error) { 58 | var newusername = req.body['new_username']; 59 | await curs.execute("update users set username = ? where username = ?", [newusername, username]); 60 | await curs.execute("update perms set username = ? where username = ?", [newusername, username]); 61 | await curs.execute("update suspend_account set username = ? where username = ?", [newusername, username]); 62 | await curs.execute("update user_settings set username = ? where username = ?", [newusername, username]); 63 | await curs.execute("update acl set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 64 | await curs.execute("update classic_acl set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 65 | await curs.execute("update documents set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 66 | await curs.execute("update threads set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 67 | await curs.execute("update edit_requests set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 68 | await curs.execute("update history set title = ? where title = ? and namespace = '사용자'", [newusername, username]); 69 | await curs.execute("update login_history set username = ? where username = ?", [newusername, username]); 70 | await curs.execute("update stars set username = ? where username = ?", [newusername, username]); 71 | await curs.execute("update useragents set username = ? where username = ?", [newusername, username]); 72 | await curs.execute("update history set username = ? where username = ? and ismember = 'author'", [newusername, username]); 73 | await curs.execute("update res set username = ? where username = ? and ismember = 'author'", [newusername, username]); 74 | await curs.execute("update res set hider = ? where hider = ?", [newusername, username]); 75 | await curs.execute("update block_history set executer = ? where executer = ? and ismember = 'author'", [newusername, username]); 76 | await curs.execute("update block_history set target = ? where target = ?", [newusername, username]); 77 | await curs.execute("update edit_requests set processor = ? where processor = ? and ismember = 'author'", [newusername, username]); 78 | await curs.execute("update edit_requests set username = ? where username = ? and ismember = 'author'", [newusername, username]); 79 | await curs.execute("update autologin_tokens set username = ? where username = ?", [newusername, username]); 80 | req.session.username = newusername; 81 | permlist[newusername] = permlist[username]; 82 | delete permlist[username]; 83 | userset[newusername] = userset[username]; 84 | delete userset[username]; 85 | return res.send(await render(req, '사용자 이름 변경', ` 86 |

${html.escape(newusername)}(으)로 이름을 변경하였습니다.

87 | `, {}, _, false, 'delete_account')); 88 | } 89 | 90 | return res.send(await render(req, '사용자 이름 변경', content, {}, _, error, 'delete_account')); 91 | }); -------------------------------------------------------------------------------- /backend/unfinished/upload.js: -------------------------------------------------------------------------------- 1 | router.all(/^\/Upload$/, async(req, res, next) => { 2 | if(!['POST', 'GET'].includes(req.method)) return next(); 3 | 4 | const licelst = await curs.execute("select title from documents where namespace = '틀' and title like '이미지 라이선스/%' order by title"); 5 | const catelst = await curs.execute("select title from documents where namespace = '분류' and title like '파일/%' order by title"); 6 | 7 | var liceopts = '', cateopts = ''; 8 | 9 | for(var lice of licelst) { 10 | liceopts += ``; 11 | } 12 | for(var cate of catelst) { 13 | cateopts += ``; 14 | } 15 | 16 | var content = ''; 17 | 18 | content = ` 19 |
20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 | ${req.method == 'GET' ? ` 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 |

[주의!] 파일문서의 라이선스(문서 본문)와 올리는 파일의 라이선스는 다릅니다. 파일의 라이선스를 올바르게 지정하였는지 확인하세요.

53 | 54 |
55 |
56 | 57 | 61 |
62 |
63 | ` : ''} 64 |
65 | 66 | 67 |
68 | 69 |

${config.getString('wiki.editagree_text', `문서 편집을 저장하면 당신은 기여한 내용을 CC-BY-NC-SA 2.0 KR으로 배포하고 기여한 문서에 대한 하이퍼링크나 URL을 이용하여 저작자 표시를 하는 것으로 충분하다는 데 동의하는 것입니다. 이 동의는 철회할 수 없습니다.`)}

70 | 71 | ${islogin(req) ? '' : `

비로그인 상태로 편집합니다. 편집 역사에 IP(${ip_check(req)})가 영구히 기록됩니다.

`} 72 | 73 |
74 | 75 |
76 |
77 | 78 | 79 | `; 80 | 81 | var error = null; 82 | 83 | if(req.method == 'POST') do { 84 | var file = req.files[0]; 85 | if(!file) { content = (error = err('alert', { code: 'file_not_uploaded' })) + content; break; } 86 | var title = req.body['document']; 87 | if(!title) { content = (error = err('alert', { code: 'validator_required', tag: 'document' })) + content; break; } 88 | var doc = processTitle(title); 89 | if(doc.namespace != '파일') { content = (error = err('alert', { msg: '업로드는 파일 이름 공간에서만 가능합니다.' })) + content; break; } 90 | if(path.extname(doc.title).toLowerCase() != path.extname(file.originalname).toLowerCase()) { 91 | content = (error = err('alert', { msg: '문서 이름과 확장자가 맞지 않습니다.' })) + content; 92 | break; 93 | } 94 | var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 1); 95 | if(aclmsg) { content = (error = err('alert', { code: 'permission_edit', msg: aclmsg })) + content; break; } 96 | 97 | if(error) break; 98 | 99 | const response = res; 100 | 101 | var request = http.request({ 102 | method: 'POST', 103 | host: hostconfig.image_host, 104 | port: hostconfig.image_port, 105 | path: '/upload', 106 | headers: { 107 | 'Content-Type': 'application/json', 108 | }, 109 | }, async res => { 110 | var data = ''; 111 | res.on('data', d => data += d); 112 | res.on('end', async () => { 113 | data = JSON.parse(data); 114 | if(data.status != 'success') { 115 | error = err('alert', { code: 'file_not_uploaded' }); 116 | return response.send(await render(req, '파일 올리기', error + content, {}, _, error, 'upload')); 117 | } 118 | await curs.execute("insert into files (title, namespace, hash) values (?, ?, ?)", [doc.title, doc.namespace, '']); // sha224 해시화 필요 119 | return response.redirect('/w/' + totitle(doc.title, doc.namespace)); 120 | }); 121 | }).on('error', async e => { 122 | error = err('alert', { msg: '파일 서버가 사용가능하지 않습니다.' }); 123 | return res.send(await render(req, '파일 올리기', error + content, {}, _, error, 'upload')); 124 | }); 125 | request.write(JSON.stringify({ 126 | filename: file.originalname, 127 | document: title, 128 | mimetype: file.mimetype, 129 | file: file.buffer.toString('base64'), 130 | })); 131 | request.end(); 132 | 133 | return; 134 | } while(0); 135 | 136 | res.send(await render(req, '파일 올리기', content, {}, _, error, 'upload')); 137 | }); -------------------------------------------------------------------------------- /backend/unfinished/special_pages.js: -------------------------------------------------------------------------------- 1 | router.get(/^\/NeededPages$/, async(req, res) => { 2 | const nslist = fetchNamespaces(); 3 | var ns = req.query['namespace']; 4 | if(!ns || !nslist.includes(ns)) ns = '문서'; 5 | 6 | if(!neededPages[ns]) neededPages[ns] = []; 7 | var ss; 8 | var st, ed; 9 | var total = neededPages[ns].length; 10 | if(!req.query['from'] && req.query['until']) { 11 | ss = Number(req.query['until']); 12 | st = ss - 100, ed = ss; 13 | if(ed > total) ed = total; 14 | if(st < 1) st = 1; 15 | 16 | } else { 17 | ss = Number(req.query['from'] || '1'); 18 | st = ss, ed = ss + 100; 19 | if(ed > total) ed = total; 20 | if(st < 1) st = 1; 21 | } 22 | 23 | const navbtns = navbtnr(total, st, ed, '/NeededPages'); 24 | const ret = neededPages[ns].slice(st - 1, ed); 25 | 26 | var content = ` 27 |
28 |
29 |
30 | 31 | 41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 | 49 |

역 링크는 존재하나 아직 작성이 되지 않은 문서 목록입니다.

50 |

이 페이지는 하루에 한번 업데이트 됩니다.

51 | 52 | ${navbtns} 53 | 54 | ' + navbtns; 60 | 61 | res.send(await render(req, '작성이 필요한 문서', content, {})); 62 | }); 63 | 64 | router.get(/^\/UncategorizedPages$/, async(req, res) => { 65 | const nslist = fetchNamespaces(); 66 | var ns = req.query['namespace']; 67 | if(!ns || !nslist.includes(ns)) ns = '문서'; 68 | 69 | var content = ` 70 |
71 |
72 |
73 | 74 | 86 |
87 | 88 |
89 | 90 |
91 |
92 |
93 | 94 |
    95 | `; 96 | 97 | let data = await curs.execute("select title, content from documents where namespace = ? order by title asc limit 100", [ns]); 98 | for(let i of data) { 99 | if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue; 100 | const d = await curs.execute("select title from backlink where title = ? and namespace = ? and type = 'category'", [i.title, ns]); 101 | if(d.length) continue; 102 | content += '
  • ' + html.escape(totitle(i.title, ns) + '') + '
  • '; 103 | } 104 | content += '
'; 105 | 106 | res.send(await render(req, '분류가 되지 않은 문서', content, {})); 107 | }); 108 | 109 | router.get(/^\/OldPages$/, async(req, res) => { 110 | const nslist = fetchNamespaces(); 111 | var ns = req.query['namespace']; 112 | if(!ns || !nslist.includes(ns)) ns = '문서'; 113 | 114 | var content = ` 115 |

편집된 지 오래된 문서의 목록입니다. (리다이렉트 제외)

116 | 117 |
    118 | `; 119 | 120 | let data = await curs.execute("select title, time, content from documents where namespace = '문서' order by cast(time as integer) asc limit 100"); 121 | for(let i of data) { 122 | if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue; 123 | content += '
  • ' + html.escape(totitle(i.title, ns) + '') + ` (수정 시각:${generateTime(toDate(i.time), timeFormat)})`; 124 | } 125 | content += '
'; 126 | 127 | res.send(await render(req, '편집된 지 오래된 문서', content, {})); 128 | }); 129 | 130 | router.get(/^\/ShortestPages$/, async function shortestPages(req, res) { 131 | var from = req.query['from']; 132 | if(!from) ns = '1'; 133 | 134 | var sql_num = 0; 135 | if(from > 0) 136 | sql_num = from - 122; 137 | else 138 | sql_num = 0; 139 | 140 | var data = await curs.execute("select title, content from documents where namespace = '문서' order by length(content) limit ?, '122'", [sql_num]); 141 | 142 | var content = ` 143 |

내용이 짧은 문서 (문서 이름공간, 리다이렉트 제외)

144 | 145 | ${navbtn(0, 0, 0, 0)} 146 | 147 |
    148 | `; 149 | 150 | for(var i of data) { 151 | if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue; 152 | content += '
  • ' + html.escape(i['title']) + ` (${i.content.length}글자)
  • `; 153 | } 154 | 155 | content += '
' + navbtn(0, 0, 0, 0); 156 | 157 | res.send(await render(req, '내용이 짧은 문서', content, {})); 158 | }); 159 | 160 | router.get(/^\/LongestPages$/, async function longestPages(req, res) { 161 | var from = req.query['from']; 162 | if(!from) ns = '1'; 163 | 164 | var sql_num = 0; 165 | if(from > 0) 166 | sql_num = from - 122; 167 | else 168 | sql_num = 0; 169 | 170 | var data = await curs.execute("select title, content from documents where namespace = '문서' order by length(content) desc limit ?, '122'", [sql_num]); 171 | 172 | var content = ` 173 |

내용이 긴 문서 (문서 이름공간, 리다이렉트 제외)

174 | 175 | ${navbtn(0, 0, 0, 0)} 176 | 177 |
    178 | `; 179 | 180 | for(var i of data) { 181 | if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue; 182 | content += '
  • ' + html.escape(i['title']) + ` (${i.content.length}글자)
  • `; 183 | } 184 | 185 | content += '
' + navbtn(0, 0, 0, 0); 186 | 187 | res.send(await render(req, '내용이 긴 문서', content, {})); 188 | }); --------------------------------------------------------------------------------