편집 요약 없음 |
편집 요약 없음 |
||
1번째 줄: | 1번째 줄: | ||
<syntaxhighlight lang=" | <syntaxhighlight lang="JavaScript"> | ||
/* 병아리 엔진 - the seed 모방 프로젝트 */ | /* 병아리 엔진 - the seed 모방 프로젝트 */ | ||
2022년 6월 28일 (화) 02:46 판
<syntaxhighlight lang="JavaScript"> /* 병아리 엔진 - the seed 모방 프로젝트 */
const http = require('http'); const https = require('https'); const path = require('path'); const geoip = require('geoip-lite'); const inputReader = require('wait-console-input'); const { SHA3 } = require('sha3'); const md5 = require('md5'); const sqlite3 = require('sqlite3').verbose(); const express = require('express'); const session = require('express-session'); const swig = require('swig'); const ipRangeCheck = require('ip-range-check'); const bodyParser = require('body-parser'); const multer = require('multer'); const fs = require('fs'); const { JSDOM } = require('jsdom'); const jquery = require('jquery'); const diff = require('./cemerick-jsdifflib.js'); const cookieParser = require('cookie-parser'); const child_process = require('child_process');
const timeFormat = 'Y-m-d H:i:s'; // 날짜 및 시간 기본 형식 const _ = undefined;
// 더 시드 모방 버전 (나중에 config.json에서 불러옴) var major = 4, minor = 12, revision = 0; var _ready = 0;
const wiki = express(); // 서버 const conn = new sqlite3.Database('./wikidata.db', () => 0); // 데이타베이스 const upload = multer(); // 파일 올리기 모듈
var wikiconfig = {}; // 위키 설정 캐시 var permlist = {}; // 권한 캐시 var userset = {}; // 사용자 설정 캐시 var skinList = []; // 스킨 목록 캐시 var skincfgs = {}; // 스킨 구성설정 캐시
var loginHistory = {}; var neededPages = {};
// https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript // 무작위 문자열 생성 function rndval(chars, length) { var result = ; var characters = chars; var charactersLength = characters.length; for ( var i = 0; i < length; i++ ) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; }
// 모듈 사용 wiki.use(bodyParser.json()); wiki.use(bodyParser.urlencoded({ extended: true })); wiki.use(upload.any()); wiki.use(express.static('public')); wiki.use(session({ key: 'kotori', secret: rndval('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 1024), cookie: { expires: false }, resave: false,
saveUninitialized: true,
})); wiki.use(cookieParser()); wiki.set('trust proxy', true);
// 업데이트 수준 const updatecode = '12';
// 사용자 권한 var perms = [ 'delete_thread', 'admin', 'editable_other_user_document', 'suspend_account', 'ipacl', 'update_thread_status', 'acl', 'nsacl', 'hide_thread_comment', 'grant', 'no_force_recaptcha', 'disable_two_factor_login', 'login_history', 'update_thread_document', 'update_thread_topic', 'aclgroup', 'api_access', ]; var disable_autoperms = ['disable_two_factor_login'];
// 로그출력 function print(x) { console.log(x); } function prt(x) { process.stdout.write(x); }
// 삐 function beep(cnt = 1) { // 경고음 재생 for(var i=1; i<=cnt; i++) prt('�'); }
// 입력받기 function input(prpt) { prt(prpt); // 일부러 이렇게. 바로하면 한글 깨짐. return inputReader.readLine(); }
// SHA-3 암호화 function sha3(str, bit) {
const hash = new SHA3(bit || 256); hash.update(str); return hash.digest('hex');
}
// 파이선 SQLite 모방 const curs = { execute(sql, params = []) { return new Promise((resolve, reject) => { if(sql.toUpperCase().startsWith("SELECT")) { conn.all(sql, params, (err, retval) => { if(err) return reject(err); conn.sd = retval; resolve(retval); }); } else { conn.run(sql, params, err => { if(err) return reject(err); resolve(0); }); } }); } };
const random = {
choice(x) { switch(typeof(x)) { case 'string': return rndval(x, 1); case 'object': return x[ Math.floor(Math.random() * x.length) ]; } }
};
// 데이타 베이스에 추가 function insert(table, obj) { var arr = []; var sql = 'insert into ' + table + ' ('; for(var item in obj) { sql += item + ', '; } sql = sql.replace(/[,]\s$/, ) + ') values ('; for(var item in obj) { sql += '?, '; arr.push(obj[item]); } sql = sql.replace(/[,]\s$/, ) + ')'; return curs.execute(sql, arr); }
// 보안을 위해... wiki.disable('x-powered-by');
// 현재 시간 타임스탬프 function getTime() { return Math.floor(new Date().getTime()); };
// 시간 포맷 function toDate(t) { var cur = getTime(); // 초 단위 시간 구분 if(Math.abs(cur - Math.floor(Number(t)) * 1000) < Math.abs(cur - Math.floor(Number(t)))) { t = Number(t) * 1000; } var date = new Date(Number(t));
var hour = date.getUTCHours(); hour = (hour < 10 ? "0" : "") + hour;
var min = date.getUTCMinutes(); min = (min < 10 ? "0" : "") + min; var sec = date.getUTCSeconds(); sec = (sec < 10 ? "0" : "") + sec; var year = date.getUTCFullYear(); var month = date.getUTCMonth() + 1; month = (month < 10 ? "0" : "") + month; var day = date.getUTCDate(); day = (day < 10 ? "0" : "") + day;
return year + "-" + month + "-" + day + " " + hour + ":" + min + ":" + sec;
}
// 시간
return ``; } generateTime.safe = true;
// 로그인 여부 function islogin(req) { if(req.session.username) return true; return false; }
// 아이디 확인 function ip_check(req, forceIP) { if(!forceIP && req.session.username) return req.session.username; else return (req.ip || '10.0.0.9').split(',')[0]; }
// 사용자설정 가져오기 function getUserset(req, str, def = ) {
str = str.replace(/^wiki[.]/, );
if(!islogin(req)) return def; const username = ip_check(req);
if(!userset[username] || !userset[username][str]) { if(!userset[username]) userset[username] = {}; userset[username][str] = def;
curs.execute("insert into user_settings (username, key, value) values (?, ?, ?)", [username, str, def]);
return def; } return userset[username][str];
}
function getUserSetting(username, str, def = ) {
str = str.replace(/^wiki[.]/, );
if(!userset[username] || !userset[username][str]) { if(!userset[username]) userset[username] = {}; userset[username][str] = def;
curs.execute("insert into user_settings (username, key, value) values (?, ?, ?)", [username, str, def]);
return def; } return userset[username][str];
}
// 더시드 엔진 4.16.0에 도입된 토론/편집요청 ID function newID() {
const a = [ 'A', 'The', ];
const b = [ "Sleepy", "Giddy", "Smooth", 'Beautiful', 'Foamy', 'Frightened', 'Lazy', 'Wonderful', 'Happy', 'Sad', 'Broken', 'Angry', 'Mad', 'Upset', 'Red', 'Blue', 'Yellow', 'Impossible', 'Working', 'Pretty', 'Relaxed', 'Cold', 'Warm', 'Hot', 'Hard', 'Loud', 'Quiet', 'New', 'Old', 'Clean', 'Washable', 'Open', 'Closed', 'Outdated', 'Fixed', 'Living', 'Locked', 'Unused', 'Used', 'Sold', 'Sharp', 'Smashed', 'Crazy', 'Free', 'Fancy', 'Ugly', 'Big', 'Small', 'Fast', 'Ugly', 'Slow', 'Dirty', 'Unclassifiable', 'Cloudy', 'Solid', 'Different', 'Hungry', 'Thirsty', 'Boorish', 'Funny', 'Puffy', 'Greasy', 'Efficacious', 'Functional', 'Undesirable', 'Naughty', 'Gray', 'Busy', 'Acceptable', 'Stormy', 'Noisy', ];
const c = [ 'And' ];
const d = [ "Station", "Discount", 'Deer', "Soup", "Ice", "Recorder", "VPN", "Installer", "Uninstaller", "Bot", "Robot", "Power", "Point", "Music", 'Event', 'Cat', 'Dog', 'Phone', 'Bush', 'Music', 'Picture', 'Lion', 'Angle', 'Horse', 'Mouse', 'Pencil', 'Box', 'Bag', 'Backpack', 'Chicken', 'CD', 'DVD', 'Diskette', 'FloppyDisk', 'Drive', 'CPU', 'Water', 'Glass', 'Memory', 'USB', 'Drive', 'Number', 'Letter', 'Fan', 'BIOS', 'Video', 'Button', 'Trash', 'Bottle', 'Cylinder', 'Ball', 'Key', 'Door', 'Plug', 'Flask', 'Cable', 'Radio', 'File', 'Disk', 'Camera', 'Titan', 'Ash', 'Tree', 'Plank', 'Script', 'Day', 'Car', 'ATV', 'Healer', 'Fox', 'Wolf', 'Carrot', 'Steak', 'Mushroom', 'Bandages', 'Berry', 'Tea', 'Charcoal', 'Limestone', 'Iron', 'Bar', 'Nail', 'Seed', 'Fiber', 'Leather', 'Fur', 'Aluminum', 'Tungsten', 'Transmission', 'Wheel', 'Fork', 'Engine', 'Transistor', 'Plastic', 'Wrench', 'Gasoline', 'Oil', 'Pickaxe', 'Hammer', 'Campfire', 'Garden', 'Furnace', 'Tower', 'Houseplant', 'Shirt', 'Sneakers', 'Helicopter', 'Trap', 'Card', 'Jar', 'Toy', 'Jet', 'Plane', 'Statement', 'Dimension', 'Toothpaste', 'Railway', 'Year', 'Stew', 'Farm', 'Zipper', 'Horses', 'Can', 'Cabbage', 'Eyes', 'Motion', 'Uncle', 'Teeth', 'Birthday', 'Downtown', ];
if(minor >= 17 || (minor == 16 && revision >= 1)) { pa = random.choice(b); pb = random.choice(b); pc = random.choice(b); pd = random.choice(d);
if(pa == pb) pb = 'Soft'; if(pa == pc) pc = 'Free'; if(pb == pc) pc = 'Cold'; return pa + pb + pc + pd; } else { pa = random.choice(a); pb = random.choice(b); pc = random.choice(c); pd = random.choice(b); pe = random.choice(d); if(['A', 'E', 'O', 'U', 'I'].includes(pb[0]) && pa == 'A')
pa = 'An';
if(pd == pb) pd = 'Soft'; return pa + pb + pc + pd + pe;
} }
// swig 필터 swig.setFilter('encode_userdoc', function encodeUserdocURL(input) { return encodeURIComponent('사용자:' + input); }); swig.setFilter('encode_doc', function encodeDocURL(input) { return encodeURIComponent(input); }); swig.setFilter('avatar_url', function(input) { return 'https://www.gravatar.com/avatar/' + md5(getUserSetting(input.username, 'email', )) + '?d=retro'; }); swig.setFilter('md5', function(input, l) { return md5(input).slice(0, (l || 33)); }); swig.setFilter('url_encode', function(input) { return encodeURIComponent(input); }); swig.setFilter('to_date', toDate); swig.setFilter('localdate', generateTime);
// 스택 (렌더러에 필요) class Stack { constructor() { this.internalArray = []; }
push(x) { this.internalArray.push(x); }
pop() { return this.internalArray.pop(); }
top() { return this.internalArray[this.internalArray.length - 1]; }
size() { return this.internalArray.length; }
empty() { return this.internalArray.length ? false : true; } };
try { hostconfig = require('./config.json'); if(hostconfig.uninitialized) throw 1; _ready = 1; if(hostconfig.theseed_version) { var sp = hostconfig.theseed_version.split('.'); major = Number(sp[0]); minor = Number(sp[1]); revision = Number(sp[2]); } if(minor >= 18) perms = perms.filter(item => !['ipacl', 'suspend_account'].includes(item)); else perms = perms.filter(item => !['aclgroup'].includes(item)); if(minor >= 2) perms = perms.filter(item => !['acl'].includes(item)); if(minor < 20) perms = perms.filter(item => !['api_access'].includes(item)); if(!(minor > 0 || (minor == 0 && revision >= 20))) perms = perms.concat(['developer', 'tribune', 'arbiter']); if(hostconfig.debug) perms.push('debug'); } catch(e) { (async function() { print('병아리 - the seed 모방 엔진에 오신것을 환영합니다.\n');
if(typeof hostconfig != 'object')
// 호스팅 설정 hostconfig = { host: input('호스트 주소: '), port: input('포트 번호: '), skin: input('기본 스킨 이름: '), search_host: '127.5.5.5', search_port: '25005', owners: [input('소유자 닉네임: ')], }; /* const frfl = [ 'js/theseed.js', 'js/jquery-2.1.4.min.js', 'js/jquery-1.11.3.min.js', 'js/intersection-observer.js', 'js/dateformatter.js',
'css/wiki.css', 'css/diffview.css', 'css/katex.min.css', ]; const skidx = { buma: 'https://github.com/LiteHell/theseed-skin-buma/archive/d77eef50a77007da391c5082b4b94818db372417.zip', liberty: 'https://github.com/namu-theseed/theseed-skin-liberty/archive/153cf78f70206643ec42e856aff8280dc21eb2c0.zip', vector: 'https://github.com/LiteHell/theseed-skin-vector/archive/51fd9afdd8000dafafd2600313e8e03df1f7fdcb.zip', namuvector: 'https://github.com/LiteHell/theseed-skin-namuvector/archive/690288e719bfe7e4abced3dc715104dd80e8f1ff.zip', marble: 'https://github.com/foxtrot-99/theseed-skin-marble/archive/refs/heads/master.zip', }; function download(path) { return new Promise((resolve, reject) => { https.get({ host: 'theseed.io', path: '/' + path, }, res => { const d = []; res.on('data', chunk => d.push(chunk)); res.on('end', () => { var ret = Buffer.from(); ret = Buffer.concat([ret, Buffer.concat(d)]); fs.writeFileS }); }); }); } if((hostconfig.uninitialized !== undefined && hostconfig.download_files) || hostconfig.uninitialized === undefined) { var chk = null; for(var f of frfl) { if(!fs.existsSync(f)) { chk = f; break; } } if(chk) { if(hostconfig.uninitialized !== undefined || (hostconfig.uninitialized === undefined && input(f + ' 파일이 없습니다. 이것은 위키 실행을 위해 필요합니다. theseed.io에서 자동으로 다운로드하시겠습니까? [Y/N]: ').toLowerCase() == 'Y')) { var dodn = 1; } } if(dodn) {
} } */ hostconfig.uninitialized = false;
// 만들 테이블 const tables = { 'documents': ['title', 'content', 'namespace', 'time'], 'history': ['title', 'namespace', 'content', 'rev', 'time', 'username', 'changes', 'log', 'iserq', 'erqnum', 'advance', 'ismember', 'edit_request_id', 'flags'], 'namespaces': ['namespace', 'locked', 'norecent', 'file'], 'users': ['username', 'password'], 'user_settings': ['username', 'key', 'value'], 'nsacl': ['namespace', 'no', 'type', 'content', 'action', 'expire'], 'config': ['key', 'value'], 'email_filters': ['address'], 'stars': ['title', 'namespace', 'username', 'lastedit'], 'perms': ['perm', 'username'], 'threads': ['title', 'namespace', 'topic', 'status', 'time', 'tnum', 'deleted', 'num'], 'res': ['id', 'content', 'username', 'time', 'hidden', 'hider', 'status', 'tnum', 'ismember', 'isadmin', 'type'], 'useragents': ['username', 'string'], 'login_history': ['username', 'ip', 'time'], 'account_creation': ['key', 'email', 'time'], 'acl': ['title', 'namespace', 'id', 'type', 'action', 'expiration', 'conditiontype', 'condition', 'ns'], 'ipacl': ['cidr', 'al', 'expiration', 'note', 'date'], 'suspend_account': ['username', 'date', 'expiration', 'note'], 'aclgroup_groups': ['name', 'admin', 'date', 'lastupdate'], 'aclgroup': ['aclgroup', 'type', 'username', 'note', 'date', 'expiration', 'id'], 'block_history': ['date', 'type', 'aclgroup', 'id', 'duration', 'note', 'executer', 'target', 'ismember', 'logid'], 'edit_requests': ['title', 'namespace', 'id', 'deleted', 'state', 'content', 'baserev', 'username', 'ismember', 'log', 'date', 'processor', 'processortype', 'lastupdate', 'processtime', 'reason', 'rev'], 'files': ['title', 'namespace', 'hash'], 'backlink': ['title', 'namespace', 'link', 'linkns', 'type', 'exist'], 'classic_acl': ['title', 'namespace', 'blockkorea', 'blockbot', 'read', 'edit', 'del', 'discuss', 'move'], 'autologin_tokens': ['username', 'token'], 'trusted_devices': ['username', 'id'], };
// 테이블 만들기 for(var table in tables) { var sql = ; sql = `CREATE TABLE ${table} ( `; for(var col of tables[table]) { sql += `${col} TEXT DEFAULT , `; } sql = sql.replace(/[,]\s$/, ); sql += `)`; await curs.execute(sql); }
fs.writeFileSync('config.json', JSON.stringify(hostconfig), 'utf8'); print('\n준비 완료되었습니다. 엔진을 다시 시작하십시오.'); process.exit(0); })(); } if(_ready) {
// 나무마크 async function markdown(content, discussion = 0, title = , flags = ) { // markdown 아니고 namumark flags = flags.split(' ');
function parseTable(content) { var data = '\n' + content + '\n';
// 캡션없는 표의 셀에 추가
for(let _tr of (data.match(/^(\|\|(((?!\|\|($|\n))[\s\S])*)\|\|)$/gim) || [])) { var tr = _tr.match(/^(\|\|(((?!\|\|($|\n))[\s\S])*)\|\|)$/gim)[0]; var otr = tr; var ntr = tr
.replace(/^[|][|]/g, '') .replace(/[|][|]$/g, '') .replace(/[|][|]/g, '')
.replace(/\n/g, '
');
data = data.replace(tr, ntr); }
var datarows = data.split('\n');
// 캡션없는 표의 시작과 끝을 감싸고, 전체에 적용되는 꾸미기 문법 적용
for(let _tr of (data.match(/^((((?!<\/td><\/tr>($|\n))[\s\S])*)<\/td><\/tr>)$/gim) || [])) { var tr = _tr.match(/^((((?!<\/td><\/tr>($|\n))[\s\S])*)<\/td><\/tr>)$/im)[0];
if ( // 표의 시작이라면(위에 || 문법 없음)
(!((befrow = (datarows[datarows.findIndex(s => s == tr.split('\n')[0]) - 1] || )).match(/^((((?!<\/td><\/tr>($|\n))[\s\S])*)<\/td><\/tr>)$/im))) && // 이전 줄이 표가 아니면
(!(befrow.match(/^(\|(((?!\|).)+)\|(((?!\|\|($|\n))[\s\S])*)\|\|)$/im))) // 캡션도 아니면 ) {
const fulloptions = (tr.replace(/<((?!table).)*>/g, ).match(/^((<([a-z0-9 ]+)=(((?!>).)+)>)+)/i) || [, ])[1];
var ts = , trs = ;
var alop, align = ((alop = (fulloptions.match(/<table\s*align=(left|center|right)>/))) || [, 'left'])[1]; if(alop) data = data.replace(tr, tr = tr.replace(alop[0], ));
var wiop, width = ((wiop = (fulloptions.match(/<table\s*width=((\d+)(px|%|))>/))) || [, ])[1]; if(wiop) { data = data.replace(tr, tr = tr.replace(wiop[0], )); trs += 'width: ' + width + '; '; }
var clop, color = ((clop = (fulloptions.match(/<table\s*color=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/))) || [, ])[1]; if(clop) { data = data.replace(tr, tr = tr.replace(clop[0], )); trs += 'color: ' + color + '; '; }
var bgop, bgcolor = ((bgop = (fulloptions.match(/<table\s*bgcolor=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/))) || [, ])[1]; if(bgop) { data = data.replace(tr, tr = tr.replace(bgop[0], )); trs += 'background-color: ' + bgcolor + '; '; }
var brop, border = ((brop = (fulloptions.match(/<table\s*bordercolor=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/))) || [, ])[1]; if(brop) { data = data.replace(tr, tr = tr.replace(brop[0], )); trs += 'border: 2px solid ' + border + '; '; }
if(trs) ts = ' style="' + trs + '"';
data = data.replace(tr, '
datarows = data.split('\n'); } if ( // 표의 끝이라면(아래에 || 문법 없음)
!((aftrow = (datarows[datarows.findIndex(s => s == (r = tr.split('\n'))[r.length - 1]) + 1] || )).match(/^((((?!<\/td><\/tr>($|\n))[\s\S])*)<\/td><\/tr>)$/im)) // 다음 줄이 표가 아니면
) { data = data.replace(tr, tr + '\n</tbody> |
');
}
data = data.replace(tr, tr.replace('', '')); datarows = data.split('\n'); } // 캡션있는 표 렌더링 for(let _tr of (data.match(/^(\|(((?!\|).)+)\|(((?!\|\|($|\n))[\s\S])*)\|\|)$/gim) || [])) { var tr = _tr.match(/^(\|(((?!\|).)+)\|(((?!\|\|($|\n))[\s\S])*)\|\|)$/im); var ec = ; if ( // 표의 시작이 아니면 건너뛰기 ((befrow = (datarows[datarows.findIndex(s => s == tr[0].split('\n')[0]) - 1] || )).match(/^((((?!<\/td><\/tr>($|\n))[\s\S])*)<\/td><\/tr>)$/im))
) continue; if ( // 표의 끝
!((aftrow = (datarows[datarows.findIndex(s => s == (r = tr[0].split('\n'))[r.length - 1]) + 1] || )).match(/^((((?!<\/td><\/tr>($|\n))[\s\S])*)<\/td><\/tr>)$/im)) // 다음 줄이 표가 아니면
) {
ec = '\n</tbody>';
}
ntr = ( ('||' + tr[4] + '||')
.replace(/^[|][|]/g, '') .replace(/[|][|]$/g, '') .replace(/[|][|]/g, '')
.replace(/\n/g, '
')
+ ec
);
const fulloptions = (ntr.replace(/<((?!table).)*>/g, ).match(/^((<([a-z0-9 ]+)=(((?!>).)+)>)+)/i) || [, ])[1];
var alop, align = ((alop = (fulloptions.match(/<table\s*align=(left|center|right)>/))) || [, 'left'])[1]; if(alop) data = data.replace(ntr, ntr = ntr.replace(alop[0], ));
var ts = , trs = ;
var wiop, width = ((wiop = (fulloptions.match(/<table\s*width=((\d+)(px|%|))>/))) || [, ])[1]; if(wiop) { data = data.replace(ntr, ntr = ntr.replace(wiop[0], )); trs += 'width: ' + width + '; '; }
var clop, color = ((clop = (fulloptions.match(/<table\s*color=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/))) || [, ])[1]; if(clop) { data = data.replace(ntr, ntr = ntr.replace(clop[0], )); trs += 'color: ' + color + '; '; }
var bgop, bgcolor = ((bgop = (fulloptions.match(/<table\s*bgcolor=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/))) || [, ])[1]; if(bgop) { data = data.replace(ntr, ntr = ntr.replace(bgop[0], )); trs += 'background-color: ' + bgcolor + '; '; }
var brop, border = ((brop = (fulloptions.match(/<table\s*bordercolor=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/))) || [, ])[1]; if(brop) { data = data.replace(ntr, ntr = ntr.replace(brop[0], )); trs += 'border: 2px solid ' + border + '; '; }
if(trs) ts = ' style="' + trs + '"';
data = data.replace(tr[0], '
datarows = data.split('\n'); }
// 셀 꾸미기
for(let _tr of (data.match(/^(((?!<\/tr>).)*)<\/tr>$/gim) || [])) { var tr = _tr.match(/^(((?!<\/tr>).)*)<\/tr>$/im)[1], ntr = tr; for(let td of (tr.match(/') .replace(/\n/g, '') .replace(/<\/tbody><tbody><\/tbody>/g, '</tbody>'); } function multiply(a, b) { if(typeof a == 'number') return a * b; var ret = ; for(let i=0; i<b; i++) ret += a; return ret; } var footnotes = new Stack(); var blocks = new Stack(); var fndisp = {}; var fnnum = 1; var fnhtml = ; var cates = ; var data = content; var doc = processTitle(title); data = html.escape(data); const xref = flags.includes('backlinkinit'); // 역링크 초기화 if(xref) await curs.execute("delete from backlink where title = ? and namespace = ?", [doc.title, doc.namespace]); const xrefl = []; if(!data.includes('\n') && data.includes('\r')) data = data.replace(/\r/g, '\n'); if(data.includes('\n') && data.includes('\r')) data = data.replace(/\r\n/g, '\n'); // 한 글자 리터럴 for(let esc of (data.match(/(?:\\)(.)/g) || [])) { const match = data.match(/(?:\\)(.)/); data = data.replace(esc, '<spannw class=nowiki>' + match[1] + '</spannw>'); } // 블록 (접기, CSS, ...) for(let block of (data.match(/([}][}][}]|[{][{][{](((?!}}}).)*)[}][}][}]|[{][{][{](((?!}}}).)*))/gim) || [])) { if(block == '}}}') { if(!blocks.size()) continue; var od = data; data = data.replace('}}}', blocks.top() + ); if(od == data) data = data.replace('\n}}}', blocks.top() + '\r'); blocks.pop(); continue; } const h = block.match(/{{{(((?!}}}).)*)/im)[1]; if(h.match(/^[#][!]folding\s/)) { // 접기 blocks.push('');const title = h.match(/^[#][!]folding\s(.*)$/)[1]; data = data.replace(, '</rawhtml></nowikiblock>'); rb = rb.replace() { if(!blocks.size()) continue; var od = data; data = data.replace('}}}', blocks.top() + ); if(od == data) data = data.replace('\n}}}', blocks.top() + '\r'); blocks.pop(); continue; }
const h = block.match(/{{{(((?!}}}).)*)/im)[1];
if(h.match(/^[#][!]folding\s/)) {
} else if(h.match(/^[#][!]wiki\s/)) {
} else if(h.match(/^[#][!]html/) && !discussion) {
} else if(block.includes('}}}')) { // 한 줄
const color = h.match(/^[#]([A-Za-z0-9]+)\s/);
const size = h.match(/^([+]|[-])([1-5])\s/);
if(color) { // 글자 색
const htmlcolor = color[1].match(/^([A-Fa-f0-9]{3,6})$/);
var col = color[1];
if(htmlcolor) {
col = '#' + htmlcolor[1];
}
data = data.replace('}}}', '');
data = data.replace('{{{' + color[0], '');
} else if(size) { // 글자 크기
data = data.replace('}}}', '');
data = data.replace('{{{' + size[0], '');
} else {
blocks.push('</nowikiblock>');
data = data.replace('{{{', '<nowikiblock>');
}
}
}
for(var item of document.querySelectorAll('nowikiblock')) {
const key = rndval('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+=/', 2048);
nwblocks[key] = item.innerHTML;
item.outerHTML = key;
}
// 토론 앵커
if(discussion) for(let res of (data.match(/(\s|^)[#](\d+)(\s|$)/g) || [])) {
const reg = res.match(/(\s|^)[#](\d+)(\s|$)/);
data = data.replace(res, reg[1] + '<a class=wiki-self-link href="#' + reg[2] + '">#' + reg[2] + '</a>' + reg[3]);
}
// 문단
data = '\r' + data;
var maxszz = 2;
var headnum = [, 0, 0, 0, 0, 0, 0];
var tochtml = '';
var cnum = 2;
var sec = 1;
for(let i=6; i; i--) {
if(data.match(RegExp(`^${multiply('=', i)}\\s.*\\s${multiply('=', i)}$`, 'm')))
maxszz = i;
}
for(let heading of (data.match(/^(=\s(((?!=).)*)\s=|==\s(((?!==).)*)\s==|===\s(((?!===).)*)\s===|====\s(((?!====).)*)\s====|=====\s(((?!=====).)*)\s=====|======\s(((?!======).)*)\s======)$/gm) || [])) {
const hr = {};
for(let i=1; i<=6; i++) {
hr[i] = heading.match(RegExp(`^${multiply('=', i)}\\s(((?!${multiply('=', i)}).)*)\\s${multiply('=', i)}$`, 'm'));
} for(let i=6; i; i--) if(hr[i]) {
if(i < cnum) for(let j=i+1; j<=6; j++) headnum[j] = 0;
cnum = i;
const title = hr[i][1];
var snum = ;
for(let j=i; j; j--) if(maxszz == j) {
for(let k=j; k<i; k++)
snum += headnum[k] + '.';
snum += ++headnum[i];
break;
}
var edlnk = ;
if(!discussion)
edlnk = `<a href="/edit/${encodeURIComponent(doc + )}?section=${sec++}" rel=nofollow>[편집]</a>`;
data = data.replace(heading, '<h' + i + ' class=wiki-heading><a href="#toc" id="s-' + snum + '">' + snum + '.</a> ' + title + edlnk + '</h' + i + '>');
var mt = i;
tochtml += multiply('', mt - maxszz + 1) + '<a href="#s-' + snum + '">' + snum + '</a>. ' + title + '' + multiply('', mt - maxszz + 1);
break;
}
}
tochtml += '';
data += '';
data = data.replace(/\n/g, '');
// 글자 꾸미기
if(minor < 8) data = data.replace(/['][']['][']['](((?![']['][']['][']).)+)[']['][']['][']/g, '$1');
data = data.replace(/['][']['](((?![']['][']).)+)[']['][']/g, '$1');
data = data.replace(/[']['](((?!['][']).)+)['][']/g, '$1');
data = data.replace(/~~(((?!~~).)+)~~/g, '$1');
data = data.replace(/--(((?!--).)+)--/g, '$1');
data = data.replace(/__(((?!__).)+)__/g, '$1');
data = data.replace(/[,][,](((?![,][,]).)+)[,][,]/g, '$1');
data = data.replace(/[^][^](((?![^][^]).)+)[^][^]/g, '$1');
// 글상자
if(minor < 7 || (minor == 7 && revision <= 4))
data = data.replace(/{{[|](((?![|]}}).)+)[|]}}/g, '$1');
// 매크로
data = data.replace(/\[br\]/gi, '<br>');
data = data.replace(/\[(date|datetime)\]/gi, generateTime(toDate(getTime()), timeFormat));
data = data.replace(/\[(tableofcontents|목차)\]/gi, tochtml);
// 각주 (1)
const fnrows = data.split('\n');
const frl = fnrows.length;
for(let fi=0; fi<frl; fi++) {
let row = fnrows[fi];
for(let fn of (row.match(/(\[[*](((?!\s).)*)\s|\])/g) || [])) {
if(fn == ']') {
if(!footnotes.size()) continue;
row = row.replace(']', '</fnstub>');
footnotes.pop();
fnrows[fi] = row;
continue;
}
if(!row.includes(']')) continue;
const reg = fn.match(/(\[[*](((?!\s).)*)\s|\])/);
row = row.replace(fn, '<fnstub' + (reg[2] ? (' name="' + reg[2] + '"') : ) + '>');
footnotes.push('[');
fnrows[fi] = row;
}
} data = fnrows.join('\n');
// 표렌더
var { document } = (new JSDOM(data.replace(/\n/g, '
'))).window;
function ft(el) {
const blks = el.querySelectorAll('dl.wiki-folding > dd, div.wiki-style, blockquote.wiki-quote');
if(blks.length) for(let el2 of blks) ft(el2);
el = (el == document ? el.querySelector('body') : el);
const ihtml = el.innerHTML;
el.innerHTML = parseTable(ihtml.replace(/
/g, '\n')).replace(/\n/g, '
');
} ft(document);
// 각주 (2)
function ff(el) {
const blks = el.querySelectorAll('fnstub');
if(blks.length) for(let el2 of blks) ff(el2);
el = (el == document ? el.querySelector('body') : el);
el = el.querySelector('fnstub');
if(!el) return;
const span = document.createElement('span');
span.innerHTML = el.innerHTML;
el.outerHTML = `<a ${el.getAttribute('name') ? 'name="' + el.getAttribute('name') + '" ' : }class=wiki-fn-content title="${span.textContent}">${el.innerHTML}</a>`;
} ff(document);
// 각주(3)
for(let item of document.querySelectorAll('a.wiki-fn-content')) {
const id = item.getAttribute('name') || fnnum;
const numid = fnnum;
item.removeAttribute('name');
item.setAttribute('href', '#fn-' + id);
fnhtml += `<a href=#rfn-${numid}>[${id}]</a> ${item.innerHTML}`;
item.innerHTML = `[${id}]`;
fnnum++;
}
if(fnhtml) fnhtml = '' + fnhtml + '';
// 한 글자 리터럴 처리
for(let item of document.querySelectorAll('spannw.nowiki')) {
item.outerHTML = item.innerHTML;
}
data = document.querySelector('body').innerHTML;
data = data.replace(/\r/g, );
data = data.replace(/
/g, '\n');
if(!discussion) data = '' + data + '';
data = data.replace(/\n/, '').replace(/\n<\/div><h(\d)/g, '<h$1').replace(/\n/g, '
');
// 사용자 문서 틀
if(!discussion && !flags.includes('preview') && doc.namespace == '사용자') {
const blockdata = await userblocked(doc.title);
if(blockdata) {
data = `
이 사용자는 차단된 사용자입니다.
이 사용자는 ${generateTime(toDate(blockdata.date), timeFormat)}에 ${blockdata.expiration == '0' ? '영구적으로' : (generateTime(toDate(blockdata.expiration), timeFormat) + '까지')} 차단되었습니다.
차단 사유: ${html.escape(blockdata.note)}
` + data;
}
if(doc.namespace == '사용자') {
if(!(minor > 0 || (minor == 0 && revision >= 20))) {
if(getperm('tribune', doc.title)) {
data = `
이 사용자는 ${config.getString('wiki.site_name', '더 시드')}의 호민관 입니다.
` + data;
} if(getperm('arbiter', doc.title)) {
data = `
이 사용자는 ${config.getString('wiki.site_name', '더 시드')}의 중재자 입니다.
` + data;
} if(getperm('admin', doc.title)) {
data = `
이 사용자는 ${config.getString('wiki.site_name', '더 시드')}의 관리자 입니다.
` + data;
} if(getperm('developer', doc.title)) {
data = `
이 사용자는 ${config.getString('wiki.site_name', '더 시드')}의 개발자 입니다.
` + data;
}
} else if(getperm('admin', doc.title)) {
data = `
이 사용자는 특수 권한을 가지고 있습니다.
` + data;
}
}
}
// 각주
if(fnhtml) data += fnhtml;
if(!discussion && doc.namespace == '분류') {
let content = ;
const dbdata = await curs.execute("select title, namespace, type from backlink where type = 'category' and link = ? and linkns = ?", [doc.title, doc.namespace]);
const _nslist = dbdata.map(item => item.namespace);
const nslistd = fetchNamespaces().filter(item => _nslist.includes(item));
const nslist = (nslistd.includes('분류') ? ['분류'] : []).concat(nslistd.filter(item => item != '분류'));
let nsopt = ;
for(let ns of nslist) {
const data = dbdata.filter(item => item.namespace == ns);
let cnt = data.length;
if(!cnt) continue;
let indexes = {};
const hj = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
const ha = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하', String.fromCharCode(55204)];
for(let item of data) {
if(!item) continue;
let chk = 0;
for(let i=0; i<ha.length-1; i++) {
const fchr = item.title[0].toUpperCase().charCodeAt(0);
if((hj[i].includes(item.title[0])) || (fchr >= ha[i].charCodeAt(0) && fchr < ha[i+1].charCodeAt(0))) {
if(!indexes[hj[i]]) indexes[hj[i]] = [];
indexes[hj[i]].push(item);
chk = 1;
break;
}
} if(!chk) {
if(!indexes[item.title[0].toUpperCase()]) indexes[item.title[0].toUpperCase()] = [];
indexes[item.title[0].toUpperCase()].push(item);
}
}
content += `
${ns == '분류' ? '하위 분류' : ('"' + doc.title + '" 분류에 속하는 ' + ns)}
전체 ${cnt}개 문서
`;
let listc = '';
let list = ;
for(let idx of Object.keys(indexes).sort()) {
list += `
${html.escape(idx)}
`;
for(let item of indexes[idx])
list += `
-
<a href="/w/${encodeURIComponent(totitle(item.title, item.namespace))}">${html.escape(item.title)}</a>
`;
list += '
';
}
listc += list + '';
content += listc;
}
data += content;
}
if(!discussion) data = '' + data + '';
// 분류
if(cates) {
data = `
분류
${cates}
` + data;
} else if(doc.namespace != '사용자' && !discussion && !flags.includes('preview')) {
data = alertBalloon('이 문서는 분류가 되어 있지 않습니다. <a href="/w/분류:분류">분류:분류</a>에서 적절한 분류를 찾아 문서를 분류해주세요!', 'info', true) + data;
}
// 리터럴블록 복구
for(var item in nwblocks) {
data = data.replace(item, nwblocks[item]);
}
return data;
}
// 위키 설정
const config = {
getString(str, def = ) {
if(wikiconfig[str] === undefined) {
curs.execute("insert into config (key, value) values (?, ?)", [str, def]);
wikiconfig[str] = def;
return def;
}
return wikiconfig[str];
}
};
// 현재 스킨
function getSkin(req) {
const def = config.getString('wiki.default_skin', hostconfig.skin);
const ret = getUserset(req, 'skin', 'default');
if(ret == 'default') return def;
if(!skinList.includes(ret)) return def;
return ret;
}
// 권한 보유여부
function getperm(perm, username) {
if(!perms.includes(perm)) return false;
if(!permlist[username]) permlist[username] = [];
return permlist[username].includes(perm);
}
// 내 권한 보유여부
function hasperm(req, perm) {
if(!islogin(req)) return false;
if(!perms.includes(perm)) return false;
if(!permlist[ip_check(req)]) permlist[ip_check(req)] = [];
return permlist[ip_check(req)].includes(perm);
}
// 비동기파일읽기
async function readFile(p, noerror = 0) {
return new Promise((resolve, reject) => {
fs.readFile(p, 'utf8', (e, r) => {
if(e) {
if(noerror) resolve();
reject(e);
} else {
resolve(r.toString());
}
});
});
}
// 비동기 파일 존재 여부
async function exists(p) {
// fs.exists는 작동안함
return new Promise((resolve, reject) => {
fs.readFile(p, (e, r) => {
// 화일이 없으니 에러
if(e) {
resolve(false);
} else {
resolve(true);
}
});
});
}
// 비동기 JSON require
async function requireAsync(p) {
return new Promise((resolve, reject) => {
fs.readFile(p, (e, r) => {
if(e) {
reject(e);
} else {
resolve( JSON.parse(r.toString()) );
}
});
});
}
// 스킨 템플릿 렌더링
async function render(req, title = , content = , varlist = {}, subtitle = , error = null, viewname = ) {
const skinInfo = {
title: title + subtitle,
viewName: viewname,
};
const perms = {
has(perm) {
try {
return permlist[ip_check(req)].includes(perm);
} catch(e) {
return false;
}
}
};
var skinconfig = skincfgs[getSkin(req)];
var templatefn = ;
if(skinconfig.override_views.includes(viewname)) {
templatefn = './skins/' + getSkin(req) + '/views/' + viewname + '.html';
} else {
templatefn = './skins/' + getSkin(req) + '/views/default.html';
}
return new Promise((resolve, reject) => {
swig.compileFile(templatefn, {}, async(e, r) => {
if(e) {
print(`[오류!] ${e.stack}`);
return resolve(`
<title>` + title + ` (스킨 렌더링 오류!)</title>
<meta charset=utf-8 />` + content);
}
varlist['skinInfo'] = skinInfo;
varlist['config'] = config;
varlist['content'] = content;
varlist['perms'] = perms;
varlist['url'] = req.path;
varlist['error'] = error;
varlist['req_ip'] = ip_check(req, 1);
if(islogin(req)) {
var user_document_discuss = null;
const udd = await curs.execute("select tnum, time from threads where namespace = '사용자' and title = ? and status = 'normal'", [req.session.username]);
if(udd.length) user_document_discuss = Math.floor(Number(udd[0].time) / 1000);
varlist['member'] = {
username: req.session.username,
};
varlist['user_document_discuss'] = user_document_discuss;
}
var output = r(varlist);
var header = '<!DOCTYPE html>\n<html><head>';
var adjs = , adcss = ;
for(var js of (hostconfig.additional_js || [])) {
adjs += `<script type="text/javascript" src="/js/${js}"></script>`;
}
for(var css of (hostconfig.additional_css || [])) {
adcss += `<link rel=stylesheet href="/css/${css}" />`;
}
header += `
<title>${title}${subtitle} - ${config.getString('wiki.site_name', '더 시드')}</title>
<meta charset=utf-8 />
<meta http-equiv=x-ua-compatible content="ie=edge" />
<meta http-equiv=x-pjax-version content="" />
<meta name=generator content="the seed" />
<meta name=application-name content="` + config.getString('wiki.site_name', '더 시드') + `" />
<meta name=mobile-web-app-capable content=yes />
<meta name=msapplication-tooltip content="` + config.getString('wiki.site_name', '더 시드') + `" />
<meta name=msapplication-starturl content="/w/` + encodeURIComponent(config.getString('wiki.front_page', 'FrontPage')) + `" />
<link rel=search type="application/opensearchdescription+xml" title="` + config.getString('wiki.site_name', '더 시드') + `" href="/opensearch.xml" />
<meta name=viewport content="width=device-width, initial-scale=1, maximum-scale=1" />
${hostconfig.use_external_css ? `
<link rel=stylesheet href="https://theseed.io/css/diffview.css" />
<link rel=stylesheet href="https://theseed.io/css/katex.min.css" />
<link rel=stylesheet href="https://theseed.io/css/wiki.css" />
` : `
<link rel=stylesheet href="/css/diffview.css" />
<link rel=stylesheet href="/css/katex.min.css" />
<link rel=stylesheet href="/css/wiki.css" />
`}${adcss}
`;
for(var css of skinconfig.auto_css_targets['*']) {
header += '<link rel=stylesheet href="/skins/' + getSkin(req) + '/' + css + '" />';
}
for(var css of (skinconfig.auto_css_targets[viewname] || [])) {
header += '<link rel=stylesheet href="/skins/' + getSkin(req) + '/' + css + '" />';
}
header += `
${hostconfig.use_external_js ? `
<script type="text/javascript" src="https://theseed.io/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="https://theseed.io/js/dateformatter.js?508d6dd4"></script>
<script type="text/javascript" src="https://theseed.io/js/intersection-observer.js?36e469ff"></script>
<script type="text/javascript" src="https://theseed.io/js/theseed.js?24141115"></script>
` : `
<script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="/js/dateformatter.js?508d6dd4"></script>
<script type="text/javascript" src="/js/intersection-observer.js?36e469ff"></script>
<script type="text/javascript" src="/js/theseed.js?24141115"></script>
`}${adjs}
`;
for(var js of skinconfig.auto_js_targets['*']) {
header += '<script type="text/javascript" src="/skins/' + getSkin(req) + '/' + js.path + '"></script>';
}
for(var js of (skinconfig.auto_js_targets[viewname] || [])) {
header += '<script type="text/javascript" src="/skins/' + getSkin(req) + '/' + js.path + '"></script>';
}
header += skinconfig.additional_heads;
header += '</head><body class="';
var ac = ;
for(var cls of skinconfig.body_classes) {
ac += cls + ' ';
}
header += ac.replace(/\s$/, ) + '">';
var footer = '</body></html>';
resolve(header + output + footer);
});
});
}
// ACL 종류
const acltype = {
read: '읽기',
edit: '편집',
move: '이동',
delete: '삭제',
create_thread: '토론 생성',
write_thread_comment: '토론 댓글',
edit_request: '편집요청',
acl: 'ACL',
};
// ACL 권한
const aclperms = {
any: '아무나',
member: '로그인된 사용자',
admin: '관리자',
member_signup_15days_ago: '가입한지 15일 지난 사용자',
suspend_account: (minor >= 18 ? undefined : '차단된 사용자'),
blocked_ipacl: (minor >= 18 ? undefined : '차단된 아이피'),
document_contributor: '해당 문서 기여자',
contributor: (minor >= 7 ? '위키 기여자' : undefined),
match_username_and_document_title: ((minor >= 6 || (minor == 5 && revision >= 9)) ? '문서 제목과 사용자 이름이 일치' : undefined),
};
// 차단된 사용자 제외 ACL 권한
const exaclperms = [
'member', 'member_signup_15days_ago', 'document_contributor', 'contributor',
];
// 오류메시지
function fetchErrorString(code, ...params) {
const codes = {
permission: '권한이 부족합니다.',
permission_read: '읽기 권한이 부족합니다.',
permission_edit: '편집 권한이 부족합니다.',
permission_move: '이동 권한이 부족합니다.',
permission_delete: '삭제 권한이 부족합니다.',
permission_create_thread: '토론 생성 권한이 부족합니다.',
permission_write_thread_comment: '토론 댓글 권한이 부족합니다.',
permission_edit_request: '편집요청 권한이 부족합니다.',
permission_acl: 'ACL 권한이 부족합니다.',
thread_not_found: '토론이 존재하지 않습니다.',
edit_request_not_found: '편집 요청을 찾을 수 없습니다.',
invalid_signup_key: '인증 요청이 만료되었거나 올바르지 않습니다.',
document_not_found: '문서를 찾을 수 없습니다.',
revision_not_found: '해당 리비전을 찾을 수 없습니다.',
validator_required: params[0] + '의 값은 필수입니다.',
invalid_username: '사용자 이름이 올바르지 않습니다.',
text_unchanged: '문서 내용이 같습니다.',
edit_conflict: '편집 도중에 다른 사용자가 먼저 편집을 했습니다.',
invalid_type_number: params[0] + '의 값은 숫자이어야 합니다.',
not_revertable: '이 리비전으로 되돌릴 수 없습니다.',
disallowed_email: '이메일 허용 목록에 있는 이메일이 아닙니다.',
file_not_uploaded: '파일이 업로드되지 않았습니다.',
username_already_exists: '사용자 이름이 이미 존재합니다.',
username_format: '사용자 이름을 형식에 맞게 입력해주세요.',
invalid_title: '문서 이름이 올바르지 않습니다.',
};
return codes[code] || code;
}
function fetchValue(code) {
const codes = {
username: '사용자 이름',
ip: 'IP 주소',
password: '암호',
password_check: '암호 확인',
};
return codes[code] || code;
}
// 오류/알림풍선
function alertBalloon(content, type = 'danger', dismissible = true, classes = , noh) {
return `
${dismissible ? `<button type=button class=close data-dismiss=alert aria-label=Close>
Close
</button>` : ``}
${
noh ? : ({
none: ,
danger: '[오류!]',
warning: ,
info: ,
success: '[경고!]'
}[type])
} ${content + }
`;
}
// 이름공간 목록
function fetchNamespaces() {
return ['문서', '틀', '분류', '파일', '사용자', '특수기능', config.getString('wiki.site_name', '더 시드'), '토론', '휴지통', '투표'].concat(hostconfig.custom_namespaces || []);
}
function err(type, obj) {
if(typeof obj == 'string') obj = { code: obj };
if(!obj.msg) obj.msg = fetchErrorString(obj.code, fetchValue(obj.tag));
if(!obj.tag) obj.tag = null;
if(type == 'alert') {
obj.toString = function() {
return alertBalloon(this.msg);
};
}
if(type == 'p') {
obj.toString = function() {
return `${html.escape(this.msg)}
`;
};
}
if(type == 'error') {
obj.toString = function() {
return this.msg;
};
}
return obj;
}
// 오류화면 표시
async function showError(req, code, ...params) {
return await render(req, minor >= 13 ? '오류' : '문제가 발생했습니다!', `${minor >= 13 ? '' : ''}${typeof code == 'object' ? (code.msg || fetchErrorString(code.code, code.tag)) : fetchErrorString(code, ...params)}${minor >= 13 ? '
' : ''}`, {}, _, _, 'error');
}
// 닉네임/아이피 파싱
function ip_pas(ip = , ismember = , nobold) {
if(ismember == 'author') {
return `${nobold ? : ''}<a href="/w/사용자:${encodeURIComponent(ip)}">${html.escape(ip)}</a>${nobold ? : ''}`;
} else {
return `<a href="/contribution/ip/${encodeURIComponent(ip)}/document">${html.escape(ip)}</a>`;
}
}
// 아이피 차단 여부
async function ipblocked(ip) {
await curs.execute("delete from ipacl where not expiration = '0' and ? > cast(expiration as integer)", [Number(getTime())]);
var ipacl = await curs.execute("select cidr, al, expiration, note from ipacl order by cidr asc limit 50");
var msg = ;
for(let row of ipacl) {
if(ipRangeCheck(ip, row.cidr)) {
if(row.al == '1') msg = '해당 IP는 반달 행위가 자주 발생하는 공용 아이피이므로 로그인이 필요합니다.
(이 메세지는 ' + (minor < 11 ? '본인이 반달을 했다기 보다는 해당 통신사를 쓰는' : '같은 인터넷 공급업체를 사용하는') + ' 다른 누군가가 해서 발생했을 확률이 높습니다.)
차단 만료일 : ' + (row.expiration == '0' ? '무기한' : new Date(Number(row.expiration))) + '
차단 사유 : ' + row.note;
else msg = 'IP가 차단되었습니다.' + (minor < 6 ? ' <a href="https://board.namu.wiki/whyiblocked">게시판</a>으로 문의해주세요.' : ) + '
차단 만료일 : ' + (row.expiration == '0' ? '무기한' : new Date(Number(row.expiration))) + '
차단 사유 : ' + row.note;
return msg;
}
} return false;
}
// 계정 차단 여부
async function userblocked(username) {
await curs.execute("delete from suspend_account where not expiration = '0' and ? > cast(expiration as integer)", [Number(getTime())]);
var data = await curs.execute("select expiration, note, date from suspend_account where username = ?", [username]);
if(data.length) {
return {
username,
expiration: data[0].expiration,
note: data[0].note,
date: data[0].date,
};
} else return false;
}
// ACL 검사
async function getacl(req, title, namespace, type, getmsg) {
var ns = await curs.execute("select id, action, expiration, condition, conditiontype from acl where namespace = ? and type = ? and ns = '1' order by cast(id as integer) asc", [namespace, type]);
var doc = await curs.execute("select id, action, expiration, condition, conditiontype from acl where title = ? and namespace = ? and type = ? and ns = '0' order by cast(id as integer) asc", [title, namespace, type]);
var flag = 0;
await curs.execute("delete from ipacl where not expiration = '0' and ? > cast(expiration as integer)", [Number(getTime())]);
var ipacl = await curs.execute("select cidr, al, expiration, note from ipacl order by cidr asc limit 50");
var data = await curs.execute("select name from aclgroup_groups");
var aclgroup = {};
for(var group of data) {
var data = await curs.execute("select id, type, username, note, expiration from aclgroup where aclgroup = ?", [group.name]);
aclgroup[group.name] = data;
}
async function f(table) {
if(!table.length && !flag) {
flag = 1;
return await f(ns);
}
var r = {
ret: 0,
m1: ,
m2: ,
msg: ,
};
for(var row of table) {
if(row.conditiontype == 'perm') {
var ret = 0;
var msg = , m1 = , m2 = ;
switch(row.condition) {
case 'any': {
ret = 1;
} break; case 'member': {
if(!islogin(req)) break;
var blocked = await userblocked(ip_check(req));
if(blocked) break;
ret = 1;
} break; case 'admin': {
if(hasperm(req, 'admin')) ret = 1;
} break; case 'member_signup_15days_ago': {
if(!islogin(req)) break;
var blocked = await userblocked(ip_check(req));
if(blocked) break;
var data = await curs.execute("select time from history where title = ? and namespace = '사용자' and username = ? and ismember = 'author' and advance = 'create' order by cast(rev as integer) asc limit 1", [ip_check(req), ip_check(req)]);
if(data.length) {
data = data[0];
if(new Date().getTime() >= Number(data.time) + 1296000000) ret = 1;
}
} break; case 'blocked_ipacl': {
if(minor < 18) for(let row of ipacl) {
if(ipRangeCheck(ip_check(req, 1), row.cidr) && !(islogin(req) && row.al == '1')) {
ret = 1;
if(row.al == '1') msg = '해당 IP는 반달 행위가 자주 발생하는 공용 아이피이므로 로그인이 필요합니다.
(이 메세지는 본인이 반달을 했다기 보다는 해당 통신사를 쓰는 다른 누군가가 해서 발생했을 확률이 높습니다.)
차단 만료일 : ' + (row.expiration == '0' ? '무기한' : new Date(Number(row.expiration))) + '
차단 사유 : ' + row.note;
else msg = 'IP가 차단되었습니다.' + (minor < 6 ? ' <a href="https://board.namu.wiki/whyiblocked">게시판</a>으로 문의해주세요.' : ) + '
차단 만료일 : ' + (row.expiration == '0' ? '무기한' : new Date(Number(row.expiration))) + '
차단 사유 : ' + row.note;
break;
}
}
} break; case 'suspend_account': {
if(!islogin(req)) break;
if(minor >= 18) break;
const data = await userblocked(ip_check(req));
if(data) {
ret = 1;
msg = '차단된 계정입니다.
차단 만료일 : ' + (data.expiration == '0' ? '무기한' : new Date(Number(data.expiration))) + '
차단 사유 : ' + data.note;
}
} break; case 'document_contributor': {
var data = await curs.execute("select rev from history where title = ? and namespace = ? and username = ? and ismember = ?", [title, namespace, ip_check(req), islogin(req) ? 'author' : 'ip']);
if(!data.length) break;
var blocked = await userblocked(ip_check(req));
if(blocked) break;
for(let row of ipacl) {
if(ipRangeCheck(ip_check(req, 1), row.cidr) && !(islogin(req) && row.al == '1')) {
blocked = 1;
break;
}
} if(blocked) break;
ret = 1;
} break; case 'contributor': {
if(minor < 7) break;
var data = await curs.execute("select rev from history where username = ? and ismember = ?", [ip_check(req), islogin(req) ? 'author' : 'ip']);
if(!data.length) break;
var blocked = await userblocked(ip_check(req));
if(blocked) break;
for(let row of ipacl) {
if(ipRangeCheck(ip_check(req, 1), row.cidr) && !(islogin(req) && row.al == '1')) {
blocked = 1;
break;
}
} if(blocked) break;
ret = 1;
} break; case 'match_username_and_document_title': {
if(minor >= 11) {
if(islogin(req) && ip_check(req) == title.split('/')[0]) ret = 1;
} else {
if(islogin(req) && ip_check(req) == title) ret = 1;
}
} break; case 'ip': {
if(!islogin(req)) ret = 1;
} break; case 'bot': {
// 나중에
} break; default: {
if(islogin(req) && hasperm(req, row.condition)) ret = 1;
}
}
if(ret) {
if(row.action == 'allow') {
r.ret = 1;
break;
} else if(row.action == 'deny') {
r.ret = 0;
r.msg = msg;
r.m1 = aclperms[row.condition] || row.condition;
break;
} else if(row.action == 'gotons' && minor >= 18) {
r = await f(ns);
break;
} else break;
} else if(row.action == 'allow') r.m2 += (aclperms[row.condition] ? aclperms[row.condition] : ('perm:' + row.condition)) + ' OR ';
} else if(row.conditiontype == 'member') {
if(ip_check(req) == row.condition && islogin(req)) {
if(row.action == 'allow') {
r.ret = 1;
break;
} else if(row.action == 'deny') {
r.ret = 0;
r.m1 = 'member:' + aclperms[row.condition] || row.condition;
break;
} else if(row.action == 'gotons' && minor >= 18) {
r = await f(ns);
break;
} else break;
} else if(row.action == 'allow') r.m2 += 'member:' + row.condition + ' OR ';
} else if(row.conditiontype == 'ip') {
if(ip_check(req, 1) == row.condition) {
if(row.action == 'allow') {
r.ret = 1;
break;
} else if(row.action == 'deny') {
r.ret = 0;
r.m1 = 'ip:' + aclperms[row.condition] || row.condition;
break;
} else if(row.action == 'gotons' && minor >= 18) {
r = await f(ns);
break;
} else break;
} else if(row.action == 'allow') r.m2 += 'ip:' + row.condition + ' OR ';
} else if(row.conditiontype == 'geoip' && (minor >= 6 || (minor == 5 && revision >= 9))) {
if(geoip.lookup(ip_check(req, 1)).country == row.condition) {
if(row.action == 'allow') {
r.ret = 1;
break;
} else if(row.action == 'deny') {
r.ret = 0;
r.m1 = 'geoip:' + aclperms[row.condition] || row.condition;
break;
} else if(row.action == 'gotons' && minor >= 18) {
r = await f(ns);
break;
} else break;
} else if(row.action == 'allow') r.m2 += 'geoip:' + row.condition + ' OR ';
} else if(row.conditiontype == 'aclgroup' && minor >= 18) {
var ag = null;
for(let item of aclgroup[row.condition]) {
if((item.type == 'ip' && ipRangeCheck(ip_check(req, 1), item.username)) || (islogin(req) && item.type == 'username' && ip_check(req) == item.username)) {
ag = item;
break;
}
} if(ag) {
if(row.action == 'allow') {
r.ret = 1;
break;
} else if(row.action == 'deny') {
r.ret = 0;
r.msg = 'ACL그룹 ' + row.condition + ' #' + ag.id + '에 있기 때문에 ' + acltype[type] + ' 권한이 부족합니다.
만료일 : ' + (ag.expiration == '0' ? '무기한' : new Date(Number(ag.expiration))) + '
사유 : ' + ag.note;
break;
} else if(row.action == 'gotons' && minor >= 18) {
r = await f(ns);
break;
} else break;
} else if(row.action == 'allow') r.m2 += 'ACL그룹 ' + row.condition + '에 속해 있는 사용자 OR ';
}
}
return r;
}
const r = await f(doc);
if(!getmsg) return r.ret;
if(!r.ret && !r.msg) {
r.msg = `${minor >= 7 && !r.m1 && !r.m2 ? 'ACL에 허용 규칙이 없기 때문에 ' : }${r.m1 && minor >= 7 ? r.m1 + '이기 때문에 ' : }${acltype[type]} 권한이 부족합니다.${r.m2 && minor >= 7 ? ' ' + r.m2.replace(/\sOR\s$/, ) + '(이)여야 합니다. ' : }`;
if(minor >= 6 || (minor == 5 && revision >= 9)) r.msg += ` 해당 문서의 <a href="/acl/${encodeURIComponent(totitle(title, namespace) + )}">ACL 탭</a>을 확인하시기 바랍니다.`;
if(type == 'edit' && getmsg != 2)
r.msg += ' 대신 <a href="/new_edit_request/' + encodeURIComponent(totitle(title, namespace) + ) + '">편집 요청</a>을 생성하실 수 있습니다.';
}
return r.msg; // 거부되었으면 오류 메시지 내용 반환, 허용은 빈 문자열
}
// 앞뒤 페이지 이동 단추
function navbtn(total, start, end, href) {
if(!href) return `
<a class="btn btn-secondary btn-sm disabled">
Past
</a>
<a class="btn btn-secondary btn-sm disabled">
Next
</a>
`; // 미구현 당시 navbtn(0, 0, 0, 0)으로 다 채웠음.
href = href.split('?')[0];
start = Number(start);
end = Number(end);
total = Number(total);
return `
<a ${end == total ? : `href="${(href + '?until=' + (end + 1))}" `}class="btn btn-secondary btn-sm${end == total ? ' disabled' : }">
Past
</a>
<a ${start <= 1 ? : `href="${(href + '?from=' + (start - 1))}" `}class="btn btn-secondary btn-sm${start <= 1 ? ' disabled' : }">
Next
</a>
`;
}
function navbtnr(total, start, end, href) {
href = href.split('?')[0];
start = Number(start);
end = Number(end);
total = Number(total);
return `
<a ${start <= 1 ? : `href="${(href + '?until=' + (start - 1))}" `}class="btn btn-secondary btn-sm${start <= 1 ? ' disabled' : }">
Past
</a>
<a ${end == total ? : `href="${(href + '?from=' + (end + 1))}" `}class="btn btn-secondary btn-sm${end == total ? ' disabled' : }">
Next
</a>
`;
}
function navbtnss(ts, te, start, end, href) {
href = href.split('?')[0];
start = start;
end = end;
return `
<a ${start == ts ? : `href="${(href + '?until=' + encodeURIComponent(start))}" `}class="btn btn-secondary btn-sm${start == ts ? ' disabled' : }">
Past
</a>
<a ${end == te ? : `href="${(href + '?from=' + encodeURIComponent(end))}" `}class="btn btn-secondary btn-sm${end == te ? ' disabled' : }">
Next
</a>
`;
}
// HTML 이스케이프
const html = {
escape(content = ) {
content = content.replace(/[&]/gi, '&');
content = content.replace(/["]/gi, '"');
content = content.replace(/[<]/gi, '<');
content = content.replace(/[>]/gi, '>');
return content;
}
};
function cacheSkinList() {
skinList = [];
skincfgs = {};
for(var dir of fs.readdirSync('./skins', { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name)) {
skinList.push(dir);
skincfgs[dir] = require('./skins/' + dir + '/config.json');
}
}
cacheSkinList();
// HTTPS 리다이렉트
wiki.use(function(req, res, next) {
if(!hostconfig.debug && hostconfig.force_https && req.headers.host && !req.secure && !req.connection.encrypted && req.protocol != 'https') {
return res.redirect('https://' + req.headers.host + req.url);
}
next();
});
// 자동 로그인 & 차단 로그아웃
wiki.all('*', async function(req, res, next) {
if(!(major > 4 || (major == 4 && minor >= 1))) {
if(islogin(req) && await userblocked(ip_check(req))) {
delete req.session.username;
return next();
}
}
if(req.session.username) {
const d = await curs.execute("select username from users where username = ?", [req.session.username]);
if(!d.length) delete req.session.username;
return next();
}
var autologin;
if(autologin = req.cookies['honoka']) {
const d = await curs.execute("select username, token from autologin_tokens where token = ?", [autologin]);
if(!d.length) {
delete req.session.username;
res.cookie('honoka', , { expires: new Date(Date.now() - 1) });
} else {
req.session.username = d[0].username;
}
}
next();
});
wiki.get(/^\/skins\/((?:(?!\/).)+)\/(.+)/, async function sendSkinFile(req, res, next) {
const skinname = req.params[0];
const filepath = req.params[1];
if(!skinList.includes(skinname))
return next();
if(decodeURIComponent(filepath).includes('./') || decodeURIComponent(filepath).includes('..')) {
return next();
}
var skinconfig = skincfgs[skinname];
/* if(!skinconfig.static_files.includes(filepath))
return next(); */
try {
res.sendFile(filepath, { root: './skins/' + skinname + '/static' });
} catch(e) {
next();
}
});
wiki.get('/js/:filepath', function sendJS(req, res) {
const filepath = req.params['filepath'];
res.sendFile(filepath, { root: './js' });
});
wiki.get('/css/:filepath', function sendCSS(req, res) {
const filepath = req.params['filepath'];
res.sendFile(filepath, { root: './css' });
});
function processTitle(d) {
const sp = d.split(':');
var ns = sp.length > 1 ? sp[0] : '문서';
var title = d;
var forceShowNamespace = false;
var nslist = fetchNamespaces();
if(nslist.includes(ns)) {
title = d.replace(ns + ':', );
if(sp[2] !== undefined && ns == '문서' && nslist.includes(sp[1])) {
forceShowNamespace = true;
}
} else {
title = d;
ns = '문서';
}
return {
title,
namespace: ns,
forceShowNamespace,
toString() {
if(forceShowNamespace || this.namespace != '문서')
return this.namespace + ':' + title;
else
return title;
}
};
}
function totitle(t, ns) {
const nslist = fetchNamespaces();
var forceShowNamespace = false;
if(ns == '문서' && nslist.includes(t.split(':')[0]) && t.split(':')[1] !== undefined)
forceShowNamespace = true;
return {
title: t,
namespace: ns,
forceShowNamespace,
toString() {
if(forceShowNamespace || this.namespace != '문서')
return this.namespace + ':' + this.title;
else
return this.title;
}
};
}
function edittype(type, ...flags) {
var ret = ;
switch(type) {
case 'create':
ret = '새 문서';
break; case 'move':
ret = flags[0] + '에서 ' + flags[1] + '(으)로 문서 이동';
break; case 'delete':
ret = '삭제';
break; case 'revert':
ret = 'r' + flags[0] + '로 되돌림';
break; case 'acl':
ret = flags[0] + '으로 ACL 변경';
}
return ret;
}
function expireopt(req) {
var disp = ['영구', '1분', '5분', '10분', '30분', '1시간', '2시간', '하루', '3일', '5일', '7일', '2주', '3주', '1개월', '6개월', '1년'];
var val = [0, 60, 300, 600, 1800, 3600, 7200, 86400, 259200, 432000, 604800, 1209600, 1814400, 2592000, 15552000, 29030400];
if(req.path == '/admin/suspend_account') {
disp = ['선택', '해제'].concat(disp);
val = [, -1].concat(val);
}
var ret = ;
for(var i=0; i<disp.length; i++) {
ret += `<option value=${val[i]}${req && req.method == 'POST' && String(req.body['expire']) === String(val[i]) ? ' selected' : }>${disp[i]}</option>`;
}
return ret;
}
wiki.get(/^\/License$/, async(req, res) => {
return res.send(await render(req, '라이선스', `
모방 타겟 the seed 버전: v${major}.${minor}.${revision}
` + await readFile('./skins/' + getSkin(req) + '/license.html') + '', {}, _, _, 'license'));
});
function redirectToFrontPage(req, res) {
res.redirect('/w/' + (config.getString('wiki.front_page', 'FrontPage')));
}
wiki.get(/^\/w$/, redirectToFrontPage);
wiki.get(/^\/w\/$/, redirectToFrontPage);
wiki.get('/', redirectToFrontPage);
wiki.get(/^\/sidebar[.]json$/, (req, res) => {
curs.execute("select time, title, namespace from history where namespace = '문서' order by cast(time as integer) desc limit 1000")
.then(async dbdata => {
var ret = [], cnt = 0, used = [];
for(var item of dbdata) {
if(used.includes(item.title)) continue;
used.push(item.title);
const del = (await curs.execute("select title from documents where title = ? and namespace = '문서'", [item.title])).length;
ret.push({
document: totitle(item.title, '문서') + ,
status: (del ? 'normal' : 'delete'),
date: Math.floor(Number(item.time) / 1000),
});
cnt++;
if(cnt > 20) break;
}
res.json(ret);
})
.catch(e => {
print(e.stack);
res.json('[]');
});
});
wiki.get(/^\/api\/sidebar$/, async(req, res) => {
var cret = [], dret = [], cnt, used;
var dbdata = await curs.execute("select time, title, namespace from history order by cast(time as integer) desc limit 1000");
cnt = 0, used = []
for(var item of dbdata) {
if(used.includes(item.title)) continue;
used.push(item.title);
const del = (await curs.execute("select title from documents where title = ? and namespace = ?", [item.title, item.namespace])).length;
cret.push({
document: totitle(item.title, item.namespace) + ,
status: (del ? 'normal' : 'delete'),
date: Math.floor(Number(item.time) / 1000),
});
cnt++;
if(cnt > 10) break;
}
var dbdata = await curs.execute("select time, num, topic, title, namespace from threads order by cast(time as integer) desc limit 1000");
cnt = 0, used = []
for(var item of dbdata) {
if(used.includes(item.num)) continue;
used.push(item.num);
dret.push({
document: totitle(item.title, item.namespace) + ,
topic: item.topic,
date: Math.floor(Number(item.time) / 1000),
id: Number(item.num),
});
cnt++;
if(cnt > 10) break;
}
res.json({
document: cret,
discuss: dret,
});
});
wiki.get(/^\/complete\/(.*)/, (req, res) => {
// 초성검색은 나중에
const query = req.params[0];
const doc = processTitle(query);
curs.execute("select title, namespace from documents where lower(title) like ? || '%' and lower(namespace) = ? limit 10", [doc.title.toLowerCase(), doc.namespace.toLowerCase()])
.then(data => {
var ret = [];
for(var i of data) {
ret.push(totitle(i.title, i.namespace) + );
}
return res.json(ret);
})
.catch(e => {
print(e.stack);
return res.status(500).json([]);
});
});
wiki.get(/^\/go\/(.*)/, (req, res) => {
const query = req.params[0];
const doc = processTitle(query);
curs.execute("select title, namespace from documents where lower(title) = ? and lower(namespace) = ?", [doc.title.toLowerCase(), doc.namespace.toLowerCase()])
.then(data => {
if(data.length) return res.redirect('/w/' + totitle(data[0].title, data[0].namespace));
else return res.redirect('/search/' + query);
})
.catch(e => {
return res.redirect('/search/' + query);
});
});
wiki.get(/^\/search\/(.*)/, async(req, res) => {
const query = req.params[0];
var content = `
찾는 문서가 없나요? 문서로 바로 갈 수 있습니다.
<a class="btn btn-secondary btn-sm" href="/w/${encodeURIComponent(query)}">'${html.escape(query)}' 문서로 가기</a>
`;
var st = new Date().getTime() / 1000;
if(!query.replace(/^(\s+)/, ).replace(/(\s+)$/, )) {
res.send(await render(req, '"' + query + '" 검색 결과', content, {}, _, _, 'search'));
}
http.request({
host: hostconfig.search_host,
port: hostconfig.search_port,
path: '/search/' + encodeURIComponent(query) + '?page=' + (req.query['page'] || '1'),
}, async rr => {
var d = ;
rr.on('data', function(chunk) {
d += chunk;
});
rr.on('end', async function() {
const ret = JSON.parse(d);
var reshtml = ;
reshtml += `
<section class=search-section>
`;
for(var item of ret.result) {
var title = totitle(item.title, item.namespace) + ;
reshtml += `
<a href="/w/${encodeURIComponent(title)}">
${html.escape(title)}
</a>
${item.content}
`;
}
reshtml += `
<nav class=pull-right>
`;
var lp = (ret.page / 10) * 10 + 10;
var max = ret.lastpage < lp ? ret.lastpage : lp;
for(var i=Math.floor(ret.page / 10) * 10 + 1; i<=max; i++) {
reshtml += `
-
<a class=page-link href="?page=${i}">${i}</a>
`;
}
reshtml += `
</nav>
</section>
`;
var et = new Date().getTime() / 1000;
content = content + `
전체 ${ret.total} 건 / 처리 시간 ${(et - st).toFixed(3).replace(/([0]+)$/, )}초
` + reshtml;
res.send(await render(req, '"' + query + '" 검색 결과', content, {}, _, _, 'search'));
});
}).on('error', async e => {
res.send(await showError(req, 'searchd_fail'));
}).end();
});
wiki.get(/^\/w\/(.*)/, async function viewDocument(req, res) {
const title = req.params[0];
if(title.replace(/\s/g, ) == ) res.redirect('/w/' + config.getString('wiki.front_page', 'FrontPage'));
const doc = processTitle(title);
var { rev } = req.query;
if(rev) {
var rawContent = await curs.execute("select content, time from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]);
var data = rawContent;
} else {
rev = null;
var rawContent = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
}
if(rev && !rawContent.length) return res.send(await showError(req, 'revision_not_found'));
var content = ;
var httpstat = 200;
var viewname = 'wiki';
var error = null;
var lastedit = undefined;
const aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) {
if(minor < 5 || (minor == 5 && revision < 7)) return res.status(403).send(await showError(req, 'permission_read'));
httpstat = 403;
error = err('error', { code: 'permission_read', msg: aclmsg });
content = '' + aclmsg + '
';
} else if(!rawContent.length) {
viewname = 'notfound';
httpstat = 404;
var data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \
where title = ? and namespace = ? order by cast(rev as integer) desc limit 3",
[doc.title, doc.namespace]);
content = `
해당 문서를 찾을 수 없습니다.
<a rel=nofollow href="/edit/` + encodeURIComponent(doc + ) + `">[새 문서 만들기]</a>
`;
if(data.length) {
content += `
이 문서의 역사
`;
for(var row of data) content += `
-
${generateTime(toDate(row.time), timeFormat)} r${row.rev} ${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : } ( 0
? 'green'
: (
Number(row.changes) < 0
? 'red'
: 'gray'
)
)
};">${row.changes}) ${ip_pas(row.username, row.ismember)} (${row.log})
`;
content += `
<a href="/history/` + encodeURIComponent(doc + ) + `">[더보기]</a>
`;
}
} else {
if(rawContent[0].content.startsWith('#redirect ')) {
const nd = rawContent[0].content.split('\n')[0].replace('#redirect ', ).split('#');
const ntitle = nd[0];
if(req.query['noredirect'] != '1' && !req.query['from']) {
return res.redirect('/w/' + encodeURIComponent(ntitle) + '?from=' + title + (nd[1] ? ('#' + nd[1]) : ));
} else {
content = '#redirect <a class=wiki-link-internal href="' + encodeURIComponent(ntitle) + (nd[1] ? ('#' + nd[1]) : ) + '">' + html.escape(ntitle) + '</a>';
}
} else content = await markdown(rawContent[0].content, 0, doc + );
if(rev && minor >= 20) content = alertBalloon('[주의!] 문서의 이전 버전(' + generateTime(toDate(data[0].time), timeFormat) + '에 수정)을 보고 있습니다. <a href="/w/' + encodeURIComponent(doc + ) + '">최신 버전으로 이동</a>', 'danger', true, , 1) + content;
if(req.query['from']) {
content = alertBalloon('<a href="' + encodeURIComponent(req.query['from']) + '?noredirect=1" class=document>' + html.escape(req.query['from']) + '</a>에서 넘어옴', 'info', false) + content;
}
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]);
lastedit = Number(data[0].time);
}
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]);
var star_count = 0, starred = false;
if(rawContent.length) {
var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]);
if(dbdata.length) starred = true;
var dd = await curs.execute("select count(title) from stars where title = ? and namespace = ?", [doc.title, doc.namespace]);
star_count = dd[0]['count(title)'];
}
res.status(httpstat).send(await render(req, totitle(doc.title, doc.namespace) + (rev ? (' (r' + rev + ' 판)') : ), content, {
star_count: minor >= 9 && rawContent.length ? star_count : undefined,
starred: minor >= 9 && rawContent.length ? starred : undefined,
date: Math.floor(lastedit / 1000),
document: doc,
rev,
user: doc.namespace == '사용자' ? true : false,
discuss_progress: dpg.length ? true : false,
}, _, error, viewname));
});
if(minor >= 9) wiki.get(/^\/member\/star\/(.*)$/, async (req, res) => {
const title = req.params[0];
if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/star/' + title));
const doc = processTitle(title);
var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]);
if(dbdata.length) return res.send(await showError(req, 'already_starred_document'));
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]);
if(!dbdata.length) return res.send(await showError(req, 'document_not_found'));
await curs.execute('insert into stars (title, namespace, username, lastedit) values (?, ?, ?, ?)', [doc.title, doc.namespace, ip_check(req), dbdata[0]['time']]);
res.redirect('/w/' + encodeURIComponent(title));
});
if(minor >= 9) wiki.get(/^\/member\/unstar\/(.*)$/, async (req, res) => {
const title = req.params[0];
if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/star/' + title));
const doc = processTitle(title);
var dbdata = await curs.execute("select title, namespace from stars where username = ? and title = ? and namespace = ?", [ip_check(req), doc.title, doc.namespace]);
if(!dbdata.length) return res.send(await showError(req, 'already_unstarred_document'));
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]);
if(!dbdata.length) return res.send(await showError(req, 'document_not_found'));
await curs.execute('delete from stars where title = ? and namespace = ? and username = ?', [doc.title, doc.namespace, ip_check(req)]);
res.redirect('/w/' + encodeURIComponent(title));
});
if(minor >= 9) wiki.get(/^\/member\/starred_documents$/, async (req, res) => {
if(!islogin(req)) return res.redirect('/member/login?redirect=' + encodeURIComponent('/member/starred_documents'));
var dd = await curs.execute("select title, namespace, lastedit from stars where username = ? order by cast(lastedit as integer) desc", [ip_check(req)]);
var content = ``;
for(var doc of dd) {
content += `
-
<a href="/w/${encodeURIComponent(totitle(doc.title, doc.namespace) + )}">${html.escape(totitle(doc.title, doc.namespace) + )}</a> (수정 시각:${generateTime(toDate(doc.lastedit), timeFormat)})
`;
}
content += '
';
res.send(await render(req, '내 문서함', content, {}, _, _, 'starred_documents'));
});
wiki.get(/^\/raw\/(.*)/, async(req, res) => {
const title = req.params[0];
const doc = processTitle(title);
const rev = req.query['rev'];
if(title.replace(/\s/g, ) === ) {
return res.send(await await showError(req, 'invalid_title'));
}
if(rev) {
var data = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]);
} else {
var data = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
}
const rawContent = data;
var content = ;
try {
if(!await getacl(req, doc.title, doc.namespace, 'read')) {
return res.send(await await showError(req, 'permission_read'));
} else {
content = rawContent[0].content;
}
} catch(e) {
return res.status(404).send(await showError(req, 'document_not_found'));
}
res.setHeader('Content-Type', 'text/plain');
res.send(content);
});
wiki.all(/^\/edit\/(.*)/, async function editDocument(req, res, next) {
if(!['POST', 'GET'].includes(req.method)) return next();
const title = req.params[0];
const doc = processTitle(title);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) {
return res.status(403).send(await showError(req, err('error', { code: 'permission_read', msg: aclmsg })));
}
if(!doc.title || ['특수기능', '투표', '토론'].includes(doc.namespace) || ((minor < 6 || (minor == 7 && revision < 3)) && doc.title.includes('://'))) return res.status(400).send(await showError(req, 'invalid_title'));
var rawContent = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
if(!rawContent[0]) rawContent = ;
else rawContent = rawContent[0].content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
var error = null;
var content = ;
var section = Number(req.query['section']) || null;
var baserev = 0;
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]);
if(data.length) baserev = data[0].rev;
var token = rndval('abcdef1234567890', 64);
var textarea = `<textarea id="textInput" name="text" wrap="soft" class=form-control>${(req.method == 'POST' ? req.body['text'] : rawContent).replace(/<\/(textarea)>/gi, '</$1>')}</textarea>`;
// 틀:나무위키 -> helptext
content = `
<form method=post id="editForm" enctype="multipart/form-data" data-title="${html.escape(doc + )}" data-recaptcha="0">
<input type="hidden" name="token" value="${token}">
<input type="hidden" name="identifier" value="${islogin(req) ? 'm' : 'i'}:${html.escape(ip_check(req))}">
<input type="hidden" name="baserev" value="${baserev}">
&<$TEXTAREA>
`;
if(minor >= 7 && minor <= 9) content = `
<a href="https://forum.theseed.io/topic/232/%EC%9D%98%EA%B2%AC%EC%88%98%EB%A0%B4-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%ED%8A%B8-%EB%AC%B8%EB%B2%95-%EB%B3%80%EA%B2%BD" target=_blank style="font-weight: bold; color: purple; font-size: 16px;">[의견수렴] 리다이렉트 문법 변경</a>
` + content;
var httpstat = 200;
var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 1);
if(aclmsg) {
error = err('alert', { code: 'permission_edit', msg: aclmsg });
content = error + content.replace('&<$TEXTAREA>', textarea).replace('<textarea', '<textarea readonly=readonly') + `
</form>
`;
httpstat = 403;
} else content += `
<label class=control-label for="summaryInput">요약</label>
<input type="text" class=form-control id="logInput" name="log" value="${req.method == 'POST' ? html.escape(req.body['log']) : }" />
<label><input ${req.cookies['agree'] == '1' ? 'checked ' : }type="checkbox" name="agree" id="agreeCheckbox" value="Y"${req.method == 'POST' && req.body['agree'] == 'Y' ? ' checked' : }> ${config.getString('wiki.editagree_text', `문서 편집을 저장하면 당신은 기여한 내용을 CC-BY-NC-SA 2.0 KR으로 배포하고 기여한 문서에 대한 하이퍼링크나 URL을 이용하여 저작자 표시를 하는 것으로 충분하다는 데 동의하는 것입니다. 이 동의는 철회할 수 없습니다.`)}</label>
${islogin(req) ? : `비로그인 상태로 편집합니다. 편집 역사에 IP(${ip_check(req)})가 영구히 기록됩니다.
`}
<button id="editBtn" class="btn btn-primary" style="width: 100px;">저장</button>
</form>
`;
if(!aclmsg && req.method == 'POST') do {
var original = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
var ex = 1;
if(!original[0]) ex = 0, original = ;
else original = original[0]['content'];
var text = req.body['text'] || ;
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
if(text.startsWith('#넘겨주기 ')) text = text.replace('#넘겨주기 ', '#redirect ');
if(text.startsWith('#redirect ')) text = text.split('\n')[0] + '\n';
if(original == text && ex) { content = (error = err('alert', { code: 'text_unchanged' })) + content; break; }
const rawChanges = text.length - original.length;
const changes = (rawChanges > 0 ? '+' : ) + String(rawChanges);
const log = req.body['log'] || ;
const agree = req.body['agree'];
if(!agree) { content = (error = err('alert', { code: 'validator_required', tag: 'agree' })) + content; break; }
const baserev = req.body['baserev'];
if(isNaN(Number(baserev))) { content = (error = err('alert', { code: 'invalid_type_number', tag: 'baserev' })) + content; break; }
var data = await curs.execute("select rev from history where rev = ? and title = ? and namespace = ?", [baserev, doc.title, doc.namespace]);
if(!data.length && ex) { content = (error = err('alert', { code: 'revision_not_found' })) + content; break; }
var data = await curs.execute("select rev from history where cast(rev as integer) > ? and title = ? and namespace = ?", [Number(baserev), doc.title, doc.namespace]);
if(data.length) {
var data = await curs.execute("select content from history where rev = ? and title = ? and namespace = ?", [baserev, doc.title, doc.namespace]);
var oc = ;
if(data.length) oc = data[0].content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
// 자동 병합
var ERROR = 1; // 0;
/*
const _tl = text.split('\n'), _nl = rawContent.split('\n'), _ol = oc.split('\n');
const tl = [], nl = [], ol = [];
// 1 - 내용이 같은 줄 찾기
while(1) {
const l1 = _tl[0], l2 = _nl[0], l3 = _ol[0];
if(l1 == l2 && l2 == l3) { // 원본, 내수정, 남의수정 모두 같으면 통과
tl.push(l1);
nl.push(l2);
ol.push(l3);
} else {
var chk = 0;
for(var j=0; j<_nl.length; j++) {
if(l1 == _nl[j]) {
tl.push(l1);
nl.push(l1);
chk = 1;
break;
} else {
tl.push(null);
nl.push(_nl[j]);
}
}
if(!chk) { // 중간에 줄이 추가된 게 아님.
}
}
_tl.splice(0, 1);
_nl.splice(0, 1);
_ol.splice(0, 1);
}*/
if(ERROR) {
error = err('alert', { code: 'edit_conflict' });
content = error + diff(oc, text, 'r' + baserev, '사용자 입력') + '자동 병합에 실패했습니다! 수동으로 수정된 내역을 아래 텍스트 박스에 다시 입력해주세요.' + content.replace('&<$TEXTAREA>', `<textarea id="textInput" name="text" wrap="soft" class=form-control>${rawContent.replace(/<\/(textarea)>/gi, '</$1>')}</textarea>`);
break;
} else if(!log) {
log = `자동 병합됨 (r${baserev})`;
}
}
const ismember = islogin(req) ? 'author' : 'ip';
var advance = 'normal';
var data = await curs.execute("select title from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
if(!data.length) {
if(['파일', '사용자'].includes(doc.namespace)) {
if((minor >= 11 && !doc.title.includes('/')) || minor < 11) {
error = err('alert', { code: 'invalid_namespace' });
content = error + content;
break; } }
advance = 'create';
await curs.execute("insert into documents (title, namespace, content) values (?, ?, ?)", [doc.title, doc.namespace, text]);
} else {
await curs.execute("update documents set content = ? where title = ? and namespace = ?", [text, doc.title, doc.namespace]);
curs.execute("update stars set lastedit = ? where title = ? and namespace = ?", [getTime(), doc.title, doc.namespace]);
}
res.cookie('agree', '1', { expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 360) });
curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]);
curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
doc.title, doc.namespace, text, String(Number(baserev) + 1), ip_check(req), getTime(), changes, log, '0', '-1', ismember, advance
]);
markdown(text, 0, doc + , 'backlinkinit');
return res.redirect('/w/' + encodeURIComponent(totitle(doc.title, doc.namespace)));
} while(0);
res.status(httpstat).send(await render(req, totitle(doc.title, doc.namespace) + ' (편집)', content.replace('&<$TEXTAREA>', textarea), {
document: doc,
body: {
baserev: String(baserev),
text: rawContent,
section,
},
helptext: ,
captcha: false,
readonly: !!aclmsg,
token,
}, , error, 'edit'));
});
wiki.post(/^\/preview\/(.*)$/, async(req, res) => {
const title = req.params[0];
const doc = processTitle(title);
var skinconfig = skincfgs[getSkin(req)];
var header = ;
for(var i=0; i<skinconfig["auto_css_targets"]['*'].length; i++) {
header += '<link rel=stylesheet href="/skins/' + getSkin(req) + '/' + skinconfig["auto_css_targets"]['*'][i] + '">';
}
for(var i=0; i<skinconfig["auto_js_targets"]['*'].length; i++) {
header += '<script type="text/javascript" src="/skins/' + getSkin(req) + '/' + skinconfig["auto_js_targets"]['*'][i]['path'] + '"></script>';
}
header += skinconfig['additional_heads'];
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset=utf8 />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
${hostconfig.use_external_css ? `
<link rel=stylesheet href="https://theseed.io/css/diffview.css">
<link rel=stylesheet href="https://theseed.io/css/katex.min.css">
<link rel=stylesheet href="https://theseed.io/css/wiki.css">
` : `
<link rel=stylesheet href="/css/diffview.css">
<link rel=stylesheet href="/css/katex.min.css">
<link rel=stylesheet href="/css/wiki.css">
`}
${hostconfig.use_external_js ? `
<script type="text/javascript" src="https://theseed.io/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="https://theseed.io/js/dateformatter.js?508d6dd4"></script>
<script type="text/javascript" src="https://theseed.io/js/intersection-observer.js?36e469ff"></script>
<script type="text/javascript" src="https://theseed.io/js/theseed.js?24141115"></script>
` : `
<script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="/js/dateformatter.js?508d6dd4"></script>
<script type="text/javascript" src="/js/intersection-observer.js?36e469ff"></script>
<script type="text/javascript" src="/js/theseed.js?24141115"></script>
`}
${header}
</head>
<body>
${html.escape(doc + )}
${await markdown(req.body['text'], 0, doc + , 'preview')}
</body>
</html>
`);
});
wiki.get(minor >= 14 ? /^\/backlink\/(.*)/ : /^\/xref\/(.*)/, async (req, res) => {
const title = req.params[0];
const doc = processTitle(title);
const flag = req.query['flag'] || '0';
const ns = req.query['namespace'] || '문서';
const type = (
flag == '1' ? (
'link'
) : (
flag == '2' ? (
'file'
) : (
flag == '4' ? (
'include'
) : flag == '8' ? (
'redirect'
) : 'all'
)
)
);
var sa = , sd = [];
if(req.query['from']) {
sa = ' and title >= ? order by title asc ';
sd.push(req.query['from']);
} else if(req.query['until']) {
sa = ' and title <= ? order by title desc ';
sd.push(req.query['until']);
} else {
sa = ' order by title asc ';
}
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] : []));
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] : []));
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));
try {
var navbtns = navbtnss(fd[0].title, ld[0].title, dbdata[0].title, dbdata[dbdata.length-1].title, (minor >= 14 ? '/backlink/' : '/xref/') + encodeURIComponent(title));
} catch(e) {
var navbtns = navbtn(0, 0, 0, 0);
}
const _nslist = dbdata.map(item => item.namespace);
const nslist = fetchNamespaces().filter(item => _nslist.includes(item));
const counts = {};
var nsopt = ;
for(var item of nslist) {
nsopt += `<option value="${item}">${item} (${dbdata.map(x => x.namespace == item).length})</option>`;
}
const data = dbdata.filter(item => item.namespace == ns);
var content = `
<fieldset class=recent-option>
<form class=form-inline method=get>
<label class=control-label>이름공간 :</label>
<select class=form-control name=namespace>${nsopt}</select>
<select class=form-control name=flag>
<option value=0>(전체)</option>
<option value=1>link</option>
<option value=2>file</option>
<option value=4>include</option>
<option value=8>redirect</option>
</select>
<button type=submit class="btn btn-primary" style="width: 5rem;">제출</button>
</form>
</fieldset>
`;
var indexes = {};
const hj = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
const ha = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하', String.fromCharCode(55204)];
for(var item of data) {
if(!item) continue;
var chk = 0;
for(var i=0; i<ha.length-1; i++) {
const fchr = item.title[0].charCodeAt(0);
if((hj[i].includes(item.title[0])) || (fchr >= ha[i].charCodeAt(0) && fchr < ha[i+1].charCodeAt(0))) {
if(!indexes[hj[i]]) indexes[hj[i]] = [];
indexes[hj[i]].push(item);
chk = 1;
break;
}
} if(!chk) {
if(!indexes[item.title[0].toUpperCase()]) indexes[item.title[0].toUpperCase()] = [];
indexes[item.title[0].toUpperCase()].push(item);
}
}
var listc = '<div' + (data.length > 6 ? ' class=wiki-category-container' : ) + '>';
var list = ;
for(var idx of Object.keys(indexes).sort()) {
list += `
${html.escape(idx)}
`;
for(var item of indexes[idx])
list += `
-
<a href="/w/${encodeURIComponent(totitle(item.title, item.namespace))}">${html.escape(totitle(item.title, item.namespace) + )}</a> (${item.type})
`;
list += '
';
} listc += list + '';
content += `
${navbtns}
${list ? listc : '해당 문서의 역링크가 존재하지 않습니다. '}
${navbtns}
`;
res.send(await render(req, title + '의 역링크', content, {
document: doc,
}, _, _, 'xref'));
});
wiki.all(/^\/revert\/(.*)/, async (req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
const title = req.params[0];
const doc = processTitle(title);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) {
return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg }));
}
var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2);
if(aclmsg) {
return res.status(403).send(await showError(req, { code: 'permission_edit', msg: aclmsg }));
}
const rev = req.query['rev'];
if(!rev || isNaN(Number(rev))) {
return res.send(await showError(req, 'revision_not_found'));
}
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]);
if(!_recentRev.length) {
return res.send(await showError(req, 'document_not_found'));
}
const dbdata = await curs.execute("select content, advance, flags from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]);
if(!dbdata.length) {
return res.send(await showError(req, 'revision_not_found'));
}
const revdata = dbdata[0];
const recentRev = _recentRev[0];
// 더 시드에서 실제로는 되돌려짐.
if(req.method == 'GET' && ['move', 'delete', 'acl', 'revert'].includes(revdata.advance)) {
return res.send(await showError(req, 'not_revertable'));
}
var content = `
<form method=post>
<textarea class=form-control rows=25 readonly>${revdata.content.replace(/<\/(textarea)>/gi, '</$1>')}</textarea>
<label>요약</label>
<input type=text class=form-control name=log />
<button type=submit class="btn btn-primary">되돌리기</button>
</form>
`;
if(req.method == 'POST') {
if(recentRev.content == revdata.content) {
return res.send(await showError(req, 'text_unchanged'));
}
await curs.execute("delete from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
await curs.execute("insert into documents (content, title, namespace) values (?, ?, ?)", [revdata.content, doc.title, doc.namespace]);
const rawChanges = revdata.content.length - recentRev.content.length;
curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
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
]);
curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]);
return res.redirect('/w/' + encodeURIComponent(doc + ));
}
res.send(await render(req, doc + ' (r' + rev + '로 되돌리기)', content, {
rev,
text: revdata.content,
document: doc,
}, _, null, 'revert'))
});
wiki.get(/^\/diff\/(.*)/, async (req, res) => {
const title = req.params[0];
const doc = processTitle(title);
const rev = req.query['rev'];
const oldrev = req.query['oldrev'];
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg }));
if(!rev || !oldrev || Number(rev) <= Number(oldrev)) return res.send(await showError(req, 'revision_not_found'));
var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]);
if(!dbdata.length) return res.send(await showError(req, 'revision_not_found'));
const revdata = dbdata[0];
var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, oldrev]);
if(!dbdata.length) return res.send(await showError(req, 'revision_not_found'));
const oldrevdata = dbdata[0];
const diffoutput = diff(oldrevdata.content, revdata.content, 'r' + oldrev, 'r' + rev);
var content = diffoutput;
res.send(await render(req, doc + ' (비교)', content, {
rev,
oldrev,
diffoutput,
document: doc,
}, _, null, 'diff'));
});
wiki.get(/^\/blame\/(.*)/, async (req, res) => {
const title = req.params[0];
const doc = processTitle(title);
const rev = req.query['rev'];
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg }));
if(!rev) {
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]);
if(d.length) rev = d[0].rev;
else return res.send(await showError(req, 'revision_not_found'));
}
var dbdata = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [doc.title, doc.namespace, rev]);
if(!dbdata.length) return res.send(await showError(req, 'revision_not_found'));
const revdata = dbdata[0];
var content = `미구현`;
res.send(await render(req, doc + ' (Blame)', content, {
rev,
document: doc,
}, _, null, 'blame'));
});
wiki.get(/^\/edit_request\/(\d+)\/preview$/, async(req, res, next) => {
const id = req.params[0];
var data = await curs.execute("select title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where not deleted = '1' and id = ?", [id]);
if(!data.length) return res.send(await showError(req, 'edit_request_not_found'));
const item = data[0];
const doc = totitle(item.title, item.namespace);
var skinconfig = skincfgs[getSkin(req)];
var header = ;
for(var i=0; i<skinconfig["auto_css_targets"]['*'].length; i++) {
header += '<link rel=stylesheet href="/skins/' + getSkin(req) + '/' + skinconfig["auto_css_targets"]['*'][i] + '">';
}
for(var i=0; i<skinconfig["auto_js_targets"]['*'].length; i++) {
header += '<script type="text/javascript" src="/skins/' + getSkin(req) + '/' + skinconfig["auto_js_targets"]['*'][i]['path'] + '"></script>';
}
header += skinconfig.additional_heads;
return res.send(`
<head>
<meta charset=utf8 />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
${hostconfig.use_external_css ? `
<link rel=stylesheet href="https://theseed.io/css/diffview.css">
<link rel=stylesheet href="https://theseed.io/css/katex.min.css">
<link rel=stylesheet href="https://theseed.io/css/wiki.css">
` : `
<link rel=stylesheet href="/css/diffview.css">
<link rel=stylesheet href="/css/katex.min.css">
<link rel=stylesheet href="/css/wiki.css">
`}
${hostconfig.use_external_js ? `
<script type="text/javascript" src="https://theseed.io/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="https://theseed.io/js/dateformatter.js?508d6dd4"></script>
<script type="text/javascript" src="https://theseed.io/js/intersection-observer.js?36e469ff"></script>
<script type="text/javascript" src="https://theseed.io/js/theseed.js?24141115"></script>
` : `
<script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="/js/dateformatter.js?508d6dd4"></script>
<script type="text/javascript" src="/js/intersection-observer.js?36e469ff"></script>
<script type="text/javascript" src="/js/theseed.js?24141115"></script>
`}
${header}
</head>
<body>
${html.escape(doc + )}
${await markdown(item.content, 0, doc + , 'preview')}
</body>
`);
});
wiki.post(/^\/edit_request\/(\d+)\/close$/, async(req, res, next) => {
const id = req.params[0];
var data = await curs.execute("select title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where not deleted = '1' and id = ?", [id]);
if(!data.length) return res.send(await showError(req, 'edit_request_not_found'));
const item = data[0];
const doc = totitle(item.title, item.namespace);
if(!(hasperm(req, 'update_thread_status') || ((islogin(req) ? 'author' : 'ip') == item.ismember && item.username == ip_check(req)))) {
return res.send(await showError(req, 'permission'));
}
if(item.state != 'open') {
return res.send(await showError(req, 'edit_request_not_open'));
}
await curs.execute("update edit_requests set state = 'closed', processor = ?, processortype = ?, processtime = ?, reason = ? where id = ?", [ip_check(req), islogin(req) ? 'author' : 'ip', getTime(), req.body['close_reason'] || , id]);
return res.redirect('/edit_request/' + id);
});
wiki.post(/^\/edit_request\/(\d+)\/accept$/, async(req, res, next) => {
const id = req.params[0];
var data = await curs.execute("select title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where not deleted = '1' and id = ?", [id]);
if(!data.length) return res.send(await showError(req, 'edit_request_not_found'));
const item = data[0];
const doc = totitle(item.title, item.namespace);
var aclmsg = await getacl(req, item.title, item.namespace, 'edit', 1);
if(aclmsg) {
return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg }));
}
if(item.state != 'open') {
return res.send(await showError(req, 'edit_request_not_open'));
}
var rev;
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]);
try {
rev = Number(data[0].rev) + 1;
} catch(e) {
rev = 1;
}
var original = await curs.execute("select content from documents where title = ? and namespace = ?", [item.title, item.namespace]);
if(!original[0]) original = ;
else original = original[0].content;
const rawChanges = item.content.length - original.length;
const changes = (rawChanges > 0 ? '+' : ) + String(rawChanges);
await curs.execute("update documents set content = ? where title = ? and namespace = ?", [item.content, item.title, item.namespace]);
curs.execute("update stars set lastedit = ? where title = ? and namespace = ?", [getTime(), item.title, item.namespace]);
curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, edit_request_id) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
item.title, item.namespace, item.content, String(rev), item.username, getTime(), changes, item.log, '0', '-1', item.ismember, 'normal', id
]);
await curs.execute("update edit_requests set state = 'accepted', processor = ?, processortype = ?, processtime = ?, rev = ? where id = ?", [ip_check(req), islogin(req) ? 'author' : 'ip', getTime(), String(rev), id]);
markdown(text, 0, doc + , 'backlinkinit');
return res.redirect('/edit_request/' + id);
});
wiki.get(minor >= 16 ? /^\/edit_request\/([a-zA-Z]+)$/ : /^\/edit_request\/(\d+)$/, async(req, res, next) => {
const id = req.params[0];
var data = await curs.execute("select title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where not deleted = '1' and id = ?", [id]);
if(!data.length) return res.send(await showError(req, 'edit_request_not_found'));
const item = data[0];
const doc = totitle(item.title, item.namespace);
const aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var data = await curs.execute("select content from history where title = ? and namespace = ? and rev = ?", [item.title, item.namespace, item.baserev]);
var base = ;
if(data.length) base = data[0].content;
var card = ;
switch(item.state) {
case 'open': {
const acceptable = await getacl(req, item.title, item.namespace, 'edit');
const closable = hasperm(req, 'update_thread_status') || ((islogin(req) ? 'author' : 'ip') == item.ismember && item.username == ip_check(req));
const editable = ((islogin(req) ? 'author' : 'ip') == item.ismember && item.username == ip_check(req));
card = `
이 편집 요청을...
${generateTime(toDate(item.lastupdate), timeFormat)}에 마지막으로 수정됨
<form id=edit-request-accept-form action="/edit_request/${id}/accept" method=post style="display: inline;">
<button${acceptable ? : ' disabled'} class="btn btn-lg btn-success${acceptable ? : ' disabled'}" data-toggle=tooltip data-placement=top title="${acceptable ? '이 편집 요청을 문서에 적용합니다.' : '이 문서를 편집할 수 있는 권한이 없습니다.'}" type=submit>Accept</button>
</form>
<button${closable ? : ' disabled'} class="btn btn-lg${closable ? : ' disabled'}" data-toggle=tooltip data-placement=top title="${closable ? '이 편집 요청을 닫습니다.' : '편집 요청을 닫기 위해서는 요청자 본인이거나 권한이 있어야 합니다.'}" type=button>Close</button>
<a class="btn btn-info btn-lg${editable ? : ' disabled'}" data-toggle=tooltip data-placement=top title="${editable ? '이 편집 요청을 수정합니다.' : '요청자 본인만 수정할 수 있습니다.'}" href="/edit_request/${id}/edit">Edit</a>
`;
} break; case 'closed': {
card = `
편집 요청이 닫혔습니다.
${generateTime(toDate(item.processtime), timeFormat)}에 ${ip_pas(item.processor, item.processortype, 1)}가 편집 요청을 닫았습니다.
${item.reason ? `사유 : ${html.escape(item.reason)}
` : }
`;
} break; case 'accepted': {
card = `
편집 요청이 승인되었습니다.
${generateTime(toDate(item.processtime), timeFormat)}에 ${ip_pas(item.processor, item.processortype, 1)}가 r${item.rev}으로 승인함.
`;
}
}
var content = `
${ip_pas(item.username, item.ismember, 1)}가 ${generateTime(toDate(item.date), timeFormat)}에 요청
<label class=control-label>기준 판</label> r${item.baserev}
<label class=control-label>편집 요약</label> ${html.escape(item.log)}
${item.state == 'open' ? `
` : }
${card}
${item.state != 'accepted' ? diff(base, item.content, '1', '2').replace('
', '
') : }
`;
var error = false;
return res.send(await render(req, doc + ' (편집 요청 ' + id + ')', content, {
document: doc,
}, _, error, 'edit_request'));
});
wiki.all(minor >= 16 ? /^\/edit_request\/([a-zA-Z]+)\/edit$/ : /^\/edit_request\/(\d+)\/edit$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
const id = req.params[0];
var data = await curs.execute("select title, namespace, state, content, baserev, username, ismember, log, date, processor, processortype, processtime, lastupdate, reason, rev from edit_requests where not deleted = '1' and id = ?", [id]);
if(!data.length) return res.send(await showError(req, 'edit_request_not_found'));
const item = data[0];
const doc = totitle(item.title, item.namespace);
const title = doc + ;
if(!((islogin(req) ? 'author' : 'ip') == item.ismember && item.username == ip_check(req))) {
return res.send(await showError(req, '자신의 편집 요청만 수정할 수 있습니다.', 1));
}
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit_request', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_edit_request', msg: aclmsg }));
var error = null;
var content = `
<form method=post id="editForm" enctype="multipart/form-data" data-title="${title}" data-recaptcha="0">
<input type="hidden" name="token" value="">
<input type="hidden" name="identifier" value="${islogin(req) ? 'm' : 'i'}:${ip_check(req)}">
<textarea id="textInput" name="text" wrap="soft" class=form-control>${html.escape(item.content)}</textarea>
<label class=control-label for="summaryInput">요약</label>
<input type="text" class=form-control id="logInput" name="log" value="${html.escape(item.log)}" />
<label><input checked type="checkbox" name="agree" id="agreeCheckbox" value="Y" /> ${config.getString('wiki.editagree_text', `문서 편집을 저장하면 당신은 기여한 내용을 CC-BY-NC-SA 2.0 KR으로 배포하고 기여한 문서에 대한 하이퍼링크나 URL을 이용하여 저작자 표시를 하는 것으로 충분하다는 데 동의하는 것입니다. 이 동의는 철회할 수 없습니다.`)}</label>
${islogin(req) ? : `비로그인 상태로 편집합니다. 편집 역사에 IP(${ip_check(req)})가 영구히 기록됩니다.
`}
<button id="editBtn" class="btn btn-primary" style="width: 100px;">저장</button>
</form>
`;
if(req.method == 'POST') do {
const agree = req.body['agree'];
if(!agree) { content = (error = err('alert', { code: 'validator_required', tag: 'agree' })) + content; break; }
await curs.execute("update edit_requests set lastupdate = ?, content = ?, log = ? where id = ?", [getTime(), req.body['text'] || , req.body['log'] || , id]);
return res.redirect('/edit_request/' + id);
} while(0);
res.send(await render(req, doc + ' (편집 요청)', content, {
document: doc,
}, , error, 'new_edit_request'));
});
wiki.all(/^\/new_edit_request\/(.*)$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
const title = req.params[0];
const doc = processTitle(title);
var data = await curs.execute("select title from documents \
where title = ? and namespace = ?",
[doc.title, doc.namespace]);
if(!data.length) return res.send(await showError(req, 'document_not_found'));
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit_request', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_edit_request', msg: aclmsg }));
var baserev;
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]);
try {
baserev = data[0].rev;
} catch(e) {
baserev = 0;
}
var rawContent = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
if(!rawContent[0]) rawContent = ;
else rawContent = rawContent[0].content;
var error = null;
var content = `
<form method=post id="editForm" enctype="multipart/form-data" data-title="${title}" data-recaptcha="0">
<input type="hidden" name="token" value="">
<input type="hidden" name="identifier" value="${islogin(req) ? 'm' : 'i'}:${ip_check(req)}">
<input type="hidden" name="baserev" value="${baserev}">
<textarea id="textInput" name="text" wrap="soft" class=form-control>${rawContent.replace(/<\/(textarea)>/gi, '</$1>')}</textarea>
<label class=control-label for="summaryInput">요약</label>
<input type="text" class=form-control id="logInput" name="log" value="">
<label><input ${req.method == 'POST' ? 'checked ' : }type="checkbox" name="agree" id="agreeCheckbox" value="Y"> ${config.getString('wiki.editagree_text', `문서 편집을 저장하면 당신은 기여한 내용을 CC-BY-NC-SA 2.0 KR으로 배포하고 기여한 문서에 대한 하이퍼링크나 URL을 이용하여 저작자 표시를 하는 것으로 충분하다는 데 동의하는 것입니다. 이 동의는 철회할 수 없습니다.`)}</label>
${islogin(req) ? : `비로그인 상태로 편집합니다. 편집 역사에 IP(${ip_check(req)})가 영구히 기록됩니다.
`}
<button id="editBtn" class="btn btn-primary" style="width: 100px;">저장</button>
</form>
`;
if(req.method == 'POST') do {
if(rawContent == req.body['text']) {
error = err('alert', { code: 'text_unchanged' });
content = error + content;
break;
}
const agree = req.body['agree'];
if(!agree) { content = (error = err('alert', { code: 'validator_required', tag: 'agree' })) + content; break; }
var data = await curs.execute("select id from edit_requests order by cast(id as integer) desc limit 1");
var id = 1;
if(data.length) id = Number(data[0].id) + 1;
await curs.execute("insert into edit_requests (title, namespace, id, state, content, baserev, username, ismember, log, date, processor, processortype, lastupdate) values (?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, , , ?)",
[doc.title, doc.namespace, id, req.body['text'] || , baserev, ip_check(req), islogin(req) ? 'author' : 'ip', req.body['log'] || , getTime(), getTime()]);
return res.redirect('/edit_request/' + id);
} while(0);
res.send(await render(req, doc + ' (편집 요청)', content, {
document: doc,
}, , error, 'new_edit_request'));
});
wiki.all(/^\/acl\/(.*)$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
const title = req.params[0];
const doc = processTitle(title);
if(['특수기능', '투표', '토론'].includes(doc.namespace) || !doc.title) return res.status(400).send(await showError(req, '문서 이름이 올바르지 않습니다.', 1));
if(minor >= 2) {
await curs.execute("delete from acl where not expiration = '0' and cast(expiration as integer) < ?", [getTime()]);
const aclmsg = await getacl(req, doc.title, doc.namespace, 'acl');
const editable = !!aclmsg;
const nseditable = hasperm(req, 'nsacl');
const types = ['read', 'edit', 'move', 'delete', 'create_thread', 'write_thread_comment', 'edit_request', 'acl'];
async function tbody(type, isns, edit) {
var ret = ;
if(isns) var data = await curs.execute("select id, action, expiration, condition, conditiontype from acl where namespace = ? and type = ? and ns = '1' order by cast(id as integer) asc", [doc.namespace, type]);
else var data = await curs.execute("select id, action, expiration, condition, conditiontype from acl where title = ? and namespace = ? and type = ? and ns = '0' order by cast(id as integer) asc", [doc.title, doc.namespace, type]);
var i = 1;
for(var row of data) {
ret += `
` : }
`;
} if(!data.length) {
ret += `
`;
}
return ret;
}
if(req.method == 'POST') {
var rawContent = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
if(!rawContent[0]) rawContent = ;
else rawContent = rawContent[0].content;
var baserev;
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]);
try {
baserev = data[0].rev;
} catch(e) {
baserev = 0;
}
const { id, after_id, mode, type, isNS, condition, action, expire } = req.body;
if(!types.includes(type)) return res.status(400).send();
if(isNS && !nseditable) return res.status(403).json({ status: fetchErrorString('permission') });
if(!nseditable && !isNS && !editable) return res.status(403).json({ status: aclmsg });
const edit = nseditable || (isNS ? nseditable : editable);
switch(mode) {
case 'insert': {
if(!['allow', 'deny'].concat(isNS || minor < 18 ? [] : ['gotons']).includes(action)) return res.status(400).send();
if(Number(expire) === NaN) return res.status(400).send();
if(!condition) return res.status(400).send();
const cond = condition.split(':');
if(cond.length != 2) return res.status(400).send();
if(!['perm', 'ip', 'member'].concat((minor >= 6 || (minor == 5 && revision >= 9)) ? ['geoip'] : []).concat(minor >= 18 ? ['aclgroup'] : []).includes(cond[0])) return res.status(400).send();
if(isNS) var data = await curs.execute("select id from acl where conditiontype = ? and condition = ? and type = ? and namespace = ? and ns = '1' order by cast(id as integer) desc limit 1", [cond[0], cond[1], type, doc.namespace]);
else var data = await curs.execute("select id from acl where conditiontype = ? and condition = ? and type = ? and title = ? and namespace = ? and ns = '0' order by cast(id as integer) desc limit 1", [cond[0], cond[1], type, doc.title, doc.namespace]);
if(data.length) return res.status(400).json({
status: fetchErrorString('acl_already_exists'),
});
if(cond[0] == 'aclgroup') {
var data = await curs.execute("select name from aclgroup_groups where name = ?", [cond[1]]);
if(!data.length) return res.status(400).json({
status: fetchErrorString('invalid_aclgroup'),
});
}
if(cond[0] == 'ip') {
if(!cond[1].match(/^([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])$/)) return res.status(400).json({
status: fetchErrorString('invalid_acl_condition'),
});
}
if(cond[0] == 'geoip') {
if(!cond[1].match(/^[A-Z][A-Z]$/)) return res.status(400).json({
status: fetchErrorString('invalid_acl_condition'),
});
}
if(cond[0] == 'member') {
var data = await curs.execute("select username from users where username = ?", [cond[1]]);
if(!data.length) return res.status(400).json({
status: fetchErrorString('invalid_username'),
});
}
if(cond[0] == 'perm') {
if(!cond[1]) return res.status(400).json({
status: fetchErrorString('invalid_acl_condition'),
});
}
const expiration = String(expire ? (getTime() + Number(expire) * 1000) : 0);
if(isNS) var data = await curs.execute("select id from acl where type = ? and namespace = ? and ns = '1' order by cast(id as integer) desc limit 1", [type, doc.namespace]);
else var data = await curs.execute("select id from acl where type = ? and title = ? and namespace = ? and ns = '0' order by cast(id as integer) desc limit 1", [type, doc.title, doc.namespace]);
if(isNS) var ff = await curs.execute("select id from acl where id = '1' and type = ? and namespace = ? and ns = '1' order by cast(id as integer) desc limit 1", [type, doc.namespace]);
else var ff = await curs.execute("select id from acl where id = '1' and type = ? and title = ? and namespace = ? and ns = '0' order by cast(id as integer) desc limit 1", [type, doc.title, doc.namespace]);
var aclid = '1';
if(data.length && ff.length) aclid = String(Number(data[0].id) + 1);
await curs.execute("insert into acl (title, namespace, id, type, action, expiration, conditiontype, condition, ns) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", [isNS ? : doc.title, doc.namespace, aclid, type, action, expire == '0' ? '0' : expiration, cond[0], cond[1], isNS ? '1' : '0']);
if(!isNS) curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
doc.title, doc.namespace, rawContent, String(Number(baserev) + 1), ip_check(req), getTime(), '0', , '0', '-1', islogin(req) ? 'author' : 'ip', 'acl', mode + ',' + type + ',' + action + ',' + condition
]);
return res.send(await tbody(type, isNS, edit));
} case 'delete': {
if(!id) return res.status(400).send();
var data = await curs.execute("select action, conditiontype, condition from acl where id = ? and type = ? and title = ? and namespace = ? and ns = ?", [id, type, isNS ? : doc.title, doc.namespace, isNS ? '1' : '0']);
if(!data.length) return res.status(400).send();
await curs.execute("delete from acl where id = ? and type = ? and title = ? and namespace = ? and ns = ?", [id, type, isNS ? : doc.title, doc.namespace, isNS ? '1' : '0']);
if(!isNS) curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
doc.title, doc.namespace, rawContent, String(Number(baserev) + 1), ip_check(req), getTime(), '0', , '0', '-1', islogin(req) ? 'author' : 'ip', 'acl', mode + ',' + type + ',' + data[0].action + ',' + data[0].conditiontype + ':' + data[0].condition
]);
return res.send(await tbody(type, isNS, edit));
} case 'move': {
if(!id || !after_id) return res.status(400).send();
if(id > after_id) { // 위로 올림
for(var i=id; i>=after_id+2; i--) {
const rndv = rndval('0123456789abcdefghijklmnopqrstuvwxyz') + ip_check(req) + getTime();
await curs.execute("update acl set id = ? where id = ? and title = ? and namespace = ? and type = ? and ns = ?", [rndv, String(i - 1), isNS ? : doc.title, doc.namespace, type, isNS ? '1' : '0']);
await curs.execute("update acl set id = ? where id = ? and title = ? and namespace = ? and type = ? and ns = ?", [String(i - 1), String(i), isNS ? : doc.title, doc.namespace, type, isNS ? '1' : '0']);
await curs.execute("update acl set id = ? where id = ? and title = ? and namespace = ? and type = ? and ns = ?", [String(i), rndv, isNS ? : doc.title, doc.namespace, type, isNS ? '1' : '0']);
}
} else { // 아래로 내림
for(var i=id; i<after_id; i++) {
const rndv = rndval('0123456789abcdefghijklmnopqrstuvwxyz') + ip_check(req) + getTime();
await curs.execute("update acl set id = ? where id = ? and title = ? and namespace = ? and type = ? and ns = ?", [rndv, String(i + 1), isNS ? : doc.title, doc.namespace, type, isNS ? '1' : '0']);
await curs.execute("update acl set id = ? where id = ? and title = ? and namespace = ? and type = ? and ns = ?", [String(i + 1), String(i), isNS ? : doc.title, doc.namespace, type, isNS ? '1' : '0']);
await curs.execute("update acl set id = ? where id = ? and title = ? and namespace = ? and type = ? and ns = ?", [String(i), rndv, isNS ? : doc.title, doc.namespace, type, isNS ? '1' : '0']);
}
}
return res.send(await tbody(type, isNS, edit));
}
}
return res.status(400).send();
} else {
var content = ``;
for(var isns of [false, true]) {
content += `
${isns ? '이름공간' : '문서'} ACL
`;
for(var type of types) {
const edit = nseditable || (isns ? nseditable : editable);
content += `
${acltype[type]}
(((?!<\/td>).)*)<\/td>/g) || [])) { var text = (td.match(/ | (((?!<\/td>).)*)<\/td>/) || [, ])[1], ot = text, ntd = td;
var notx = text.replace(/^((<([a-z0-9():\| -]+)((=(((?!>).)+))*)>)+)/i, ); var attr = , tds = , cs = , rs = ; const fulloptions = (td.replace(/(<table([a-z0-9 ]+)=(((?!>).)+)>)/g, ).match(/^ | ((<([a-z0-9():\|\^ -]+)((=(((?!>).)+))*)>)+)/i) || [, ])[1];
// 정렬1 if(notx.startsWith(' ') && notx.endsWith(' ')) { tds += 'text-align: center; '; } else if(notx.startsWith(' ') && !notx.endsWith(' ')) { tds += 'text-align: right; '; } // 정렬2 var align = (fulloptions.match(/<([(]|[:]|[)])>/) || [, ])[1]; if(align) { tds += 'text-align: ' + ( align == '(' ? ( 'left' ) : ( align == ')' ? ( 'right' ) : ( 'center' ) ) ) + '; '; ntd = ntd.replace(/<([(]|[:]|[)])>/, ); } // 너비 var width = (fulloptions.match(/<width=((\d+)(px|%|))>/) || [, ])[1]; if(width) { tds += 'width: ' + width + '; '; ntd = ntd.replace(/<width=((\d+)(px|%|))>/, ); } // 높이 var height = (fulloptions.match(/<height=((\d+)(px|%|))>/) || [, ])[1]; if(height) { tds += 'height: ' + height + '; '; ntd = ntd.replace(/<height=((\d+)(px|%|))>/, ); } // 가로 합치기 var colspan = (fulloptions.match(/<[-](\d+)>/) || [, ])[1]; if(colspan) { cs = colspan; ntd = ntd.replace(/<[-](\d+)>/, ); } // 세로 합치기 & 정렬 var rowopt = (fulloptions.match(/<([^]|[v]|)[|](\d+)>/) || [, , ]); if(rowopt[2]) { rs = rowopt[2]; switch(rowopt[1]) { case '^': tds += 'vertical-align: top; '; break; case 'v': tds += 'vertical-align: bottom; '; } ntd = ntd.replace(/<([^]|[v]|)[|](\d+)>/, ); } // 셀 배경색 var bgcolor = (fulloptions.match(/<((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/) || [, ])[1]; if(bgcolor) { tds += 'background-color: ' + bgcolor + '; '; ntd = ntd.replace(/<((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/, ); } // 셀 배경색 2 var bgcolor = (fulloptions.match(/<bgcolor=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/) || [, ])[1]; if(bgcolor) { tds += 'background-color: ' + bgcolor + '; '; ntd = ntd.replace(/<bgcolor=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/, ); } // 글자색 var color = (fulloptions.match(/<color=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/) || [, ])[1]; if(color) { tds += 'color: ' + color + '; '; ntd = ntd.replace(/<color=((#[a-fA-F0-9]{3,6})|([a-zA-Z]+))>/, ); } if(tds) attr += ' style="' + tds + '"'; if(cs) attr += ' colspan=' + cs; if(rs) attr += ' rowspan=' + rs; ntd = ntd.replace(/ | /, '<td' + attr + '>');
ntr = ntr.replace(td, ntd); } data = data.replace(tr, ntr) } return data .replace(/^\n/, ) .replace(/\n$/, ) .replace(/<tbody>\n/g, '<tbody>') .replace(/\n<\/tbody>/g, '<tbody>') .replace(/<\/tr>\n/g, ' | |
1 vs. 2 | <a target=_blank href="/edit_request/' + id + '/preview">(미리보기)</a> | |||
---|---|---|---|---|
${i++} | ${row.conditiontype}:${row.condition} | ${({
allow: '허용', deny: '거부', gotons: '이름공간ACL 실행', })[row.action]} |
${row.expiration == '0' ? '영구' : generateTime(toDate(row.expiration), timeFormat)} | ${edit ? `<button type="submit" class="btn btn-danger btn-sm">삭제</button> |
(규칙이 존재하지 않습니다. ${isns ? '모두 거부됩니다.' : '이름공간 ACL이 적용됩니다.'}) |
<colgroup>
<col style="width: 60px">
<col>
<col style="width: 80px">
<col style="width: 200px">
<col style="width: 60px;">
</colgroup>
<thead>
</thead>
<tbody class="seed-acl-tbody">
`;
content += await tbody(type, isns, edit);
content += `
</tbody>
No | Condition | Action | Expiration |
---|
`;
if(edit) {
var aclpermopt = ;
for(var prm in aclperms) {
if(!aclperms[prm]) continue;
aclpermopt += `<option value=${prm}>${aclperms[prm]}${minor >= 18 ? : (exaclperms.includes(prm) ? ' [*]' : )}</option>`;
}
content += `
<label class=control-label>Condition :</label>
<select class="seed-acl-add-condition-type form-control" id="permTypeWTC">
<option value="perm">권한</option>
<option value="member">사용자</option>
<option value="ip">아이피</option>
${(minor >= 6 || (minor == 5 && revision >= 9)) ? `<option value="geoip">GeoIP</option>` : }
${minor >= 18 ? `<option value="aclgroup">ACL그룹</option>` : }
</select>
<select class="seed-acl-add-condition-value-perm form-control" id="permTextWTC">
${aclpermopt}
</select>
<input class="seed-acl-add-condition-value form-control" style="display: none;" type="text">
<label class=control-label>Action :</label>
<select class="seed-acl-add-action form-control">
<option value="allow">허용</option>
<option value="deny">거부</option>
${isns || minor < 18 ? : `<option value="gotons">이름공간ACL 실행</option>`}
</select>
<label class=control-label>Duration :</label>
<select class="form-control seed-acl-add-expire">
${expireopt(req)}
</select>
<button type="submit" class="btn btn-primary seed-acl-add-btn">추가</button>
${minor >= 18 ? : `[*] 차단된 사용자는 포함되지 않습니다.`}
`;
} content += `
`;
}
content += `
`;
}
return res.send(await render(req, doc + ' (ACL)', content, {
document: doc,
}, , false, 'acl'));
}
} else {
if(!hasperm(req, 'acl')) return res.send(await showError(req, 'permission'));
// 내가 나무위키 자체는 ACL 개편 전에도 했지만, ACL 인터페이스는 개편 후 처음 접했음. 원본 HTML 코드는 모르고 캡춰 화면 보고 내 나름대로 씀.
var content = `
<form method=post>
<label>읽기 : </label>
<select name=read class=form-control>
<option value=everyone>모두</option>
<option value=member>로그인한 사용자</option>
<option value=admin>괸리자</option>
</select>
<label>편집 : </label>
<select name=edit class=form-control>
<option value=everyone>모두</option>
<option value=member>로그인한 사용자</option>
<option value=admin>괸리자</option>
</select>
<label>삭제 : </label>
<select name=delete class=form-control>
<option value=everyone>모두</option>
<option value=member>로그인한 사용자</option>
<option value=admin>괸리자</option>
</select>
<label>토론 : </label>
<select name=discuss class=form-control>
<option value=everyone>모두</option>
<option value=member>로그인한 사용자</option>
<option value=admin>괸리자</option>
</select>
<label>이동 : </label>
<select name=move class=form-control>
<option value=everyone>모두</option>
<option value=member>로그인한 사용자</option>
<option value=admin>괸리자</option>
</select>
<label>요약 : </label>
<input name=log type=text id=logInput style="width: 100%;" />
<button type=submit>삽입</button>
</form>
`;
return res.send(await render(req, doc + ' (ACL)', content, {
document: doc,
}, , false, 'acl'));
}
});
wiki.get(/^\/RecentChanges$/, async function recentChanges(req, res) {
var flag = req.query['logtype'];
if(!['all', 'create', 'delete', 'move', 'revert'].includes(flag)) flag = 'all';
if(flag == 'all') flag = '%';
var data = await curs.execute("select flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \
where " + (flag == '%' ? "not namespace = '사용자' and " : ) + "advance like ? order by cast(time as integer) desc limit 100",
[flag]);
var content = `
<colgroup>
<col>
<col style="width: 25%;">
<col style="width: 22%;">
</colgroup>
<thead id>
</thead>
<tbody id>
`;
for(var row of data) {
var title = totitle(row.title, row.namespace) +
content += `
<tr${(row.log.length > 0 || row.advance != 'normal' ? ' class=no-line' :
`;
if(row.log.length > 0 || row.advance != 'normal') {
content += `
`;
}
}
content += `
</tbody>
항목
수정자
수정 시간
<a href="/w/${encodeURIComponent(title)}">${html.escape(title)}</a>
<a href="/history/${encodeURIComponent(title)}">[역사]</a>
${
Number(row.rev) > 1
? '<a \href="/diff/' + encodeURIComponent(title) + '?rev=' + row.rev + '&oldrev=' + String(Number(row.rev) - 1) + '">[비교]</a>'
:
}
<a href="/discuss/${encodeURIComponent(title)}">[토론]</a>
( 0
? 'green'
: (
Number(row.changes) < 0
? 'red'
: 'gray'
)
)
};">${row.changes})
${ip_pas(row.username, row.ismember)}
${generateTime(toDate(row.time), timeFormat)}
${row.log} ${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : }
;
)}>
`;
res.send(await render(req, '최근 변경내역', content, {}));
});
wiki.get(/^\/contribution\/(ip|author)\/(.+)\/document$/, async function documentContributionList(req, res) {
const ismember = req.params[0];
const username = req.params[1];
var moredata = [];
if(ismember == 'author' && username.toLowerCase() == 'namubot') {
var data = [];
} else {
var data = await curs.execute("select flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \
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", [
Number(getTime()) - 2592000000, ismember, username.toLowerCase()
]);
// 2018년 더시드 업데이트로 최근 30일을 넘어선 기록을 최대 100개까지 볼 수 있었음
var tt = Number(getTime()) + 12345;
if(data.length) tt = Number(data[data.length - 1].time);
if(data.length < 100 && minor >= 8)
moredata = await curs.execute("select flags, title, namespace, rev, time, changes, log, iserq, erqnum, advance, ismember, username from history \
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 ?", [
tt, ismember, username.toLowerCase(), 100 - data.length
]);
data = data.concat(moredata);
}
var content = `
최근 30일동안의 기여 목록 입니다.
<colgroup>
<col>
<col style="width: 25%;">
<col style="width: 22%;">
</colgroup>
<thead id>
</thead>
<tbody id>
`;
for(var row of data) {
var title = totitle(row.title, row.namespace) +
content += `
<tr${(row.log.length > 0 || row.advance != 'normal' ? ' class=no-line' :
`;
if(row.log.length > 0 || row.advance != 'normal') {
content += `
`;
}
}
content += `
</tbody>
문서
수정자
수정 시간
<a href="/w/${encodeURIComponent(title)}">${html.escape(title)}</a>
<a href="/history/${encodeURIComponent(title)}">[역사]</a>
${
Number(row.rev) > 1
? '<a \href="/diff/' + encodeURIComponent(title) + '?rev=' + row.rev + '&oldrev=' + String(Number(row.rev) - 1) + '">[비교]</a>'
:
}
<a href="/discuss/${encodeURIComponent(title)}">[토론]</a>
( 0
? 'green'
: (
Number(row.changes) < 0
? 'red'
: 'gray'
)
)
};">${row.changes})
${ip_pas(row.username, row.ismember)}
${generateTime(toDate(row.time), timeFormat)}
${row.log} ${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : }
;
)}>
`;
res.send(await render(req, `"${username}" 기여 목록`, content, {}));
});
wiki.get(/^\/RecentDiscuss$/, async function recentDicsuss(req, res) {
var logtype = req.query['logtype'];
if(!logtype) logtype = 'all';
var content = `
<colgroup>
<col>
<col style="width: 22%; min-width: 100px;">
</colgroup>
<thead>
</thead>
<tbody id>
`;
var trds;
switch(logtype) {
case 'normal_thread':
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");
break; case 'old_thread':
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");
break; case 'closed_thread':
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");
break; case 'open_editrequest':
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");
break; case 'closed_editrequest':
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");
break; case 'accepted_editrequest':
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");
break; default:
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");
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");
trds = data1.concat(data2).sort((l, r) => ((r.date || r.time) - (l.date || l.time))).slice(0, 120);
}
for(var trd of trds) {
const title = totitle(trd.title, trd.namespace) +
content += `
`;
}
content += `
</tbody>
항목
수정 시간
${trd.state
? `<a href="/edit_request/${minor >= 16 ? trd.slug : trd.id}">편집 요청 ${html.escape(minor >= 16 ? trd.slug : trd.id)}</a> (<a href="/discuss/${encodeURIComponent(title)}">${html.escape(title)}</a>)`
: `<a href="/thread/${minor >= 16 ? trd.slug : trd.tnum}">${html.escape(trd.topic)}</a> (<a href="/discuss/${encodeURIComponent(title)}">${html.escape(title)}</a>)`
}
${generateTime(toDate(trd.time || trd.date), timeFormat)}
;
`;
res.send(await render(req, '최근 토론', content, {}));
});
wiki.get(/^\/contribution\/(ip|author)\/(.+)\/discuss$/, async function discussionLog(req, res) {
const ismember = req.params[0];
const username = req.params[1];
var dd = await curs.execute("select id, tnum, time, username, ismember from res \
where cast(time as integer) >= ? and ismember = ? and lower(username) = ? order by cast(time as integer) desc", [
Number(getTime()) - 2592000000, ismember, username.toLowerCase()
]);
var content = `
최근 30일동안의 기여 목록 입니다.
<colgroup>
<col>
<col style="width: 25%;">
<col style="width: 22%;">
</colgroup>
<thead id>
</thead>
<tbody id>
`;
for(var row of dd) {
const td = (await curs.execute("select title, namespace, topic from threads where tnum = ?", [row.tnum]))[0];
const title = totitle(td.title, td.namespace) +
content += `
`;
}
content += `
</tbody>
항목
수정자
수정 시간
<a href="/thread/${row.tnum}#${row.id}">#${row.id} ${html.escape(td['topic'])}</a> (<a href="/w/${encodeURIComponent(title)}">${html.escape(title)}</a>)
${ip_pas(row.username, row.ismember)}
${generateTime(toDate(row.time), timeFormat)}
;
`;
res.send(await render(req, `"${username}" 기여 목록`, content, {}));
});
wiki.get(/^\/history\/(.*)/, async function viewHistory(req, res) {
var title = req.params[0];
const doc = processTitle(title);
title = totitle(doc.title, doc.namespace);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.status(403).send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var total = (await curs.execute("select count(rev) from history where title = ? and namespace = ?", [doc.title, doc.namespace]))[0]['count(rev)'];
var data;
const from = req.query['from'];
const until = req.query['until'];
if(from) {
data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username, edit_request_id from history \
where title = ? and namespace = ? and (cast(rev as integer) <= ? AND cast(rev as integer) > ?) \
order by cast(rev as integer) desc",
[doc.title, doc.namespace, Number(from), Number(from) - 30]);
} else if(until) {
data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username, edit_request_id from history \
where title = ? and namespace = ? and (cast(rev as integer) >= ? AND cast(rev as integer) < ?) \
order by cast(rev as integer) desc",
[doc.title, doc.namespace, Number(until), Number(until) + 30]);
} else {
data = await curs.execute("select flags, rev, time, changes, log, iserq, erqnum, advance, ismember, username, edit_request_id from history \
where title = ? and namespace = ? order by cast(rev as integer) desc limit 30",
[doc.title, doc.namespace]);
}
if(!data.length) return res.send(await showError(req, 'document_not_found'));
const navbtns = navbtn(total, data[data.length-1].rev, data[0].rev, '/history/' + encodeURIComponent(title));
var content = `
<button id="diffbtn" class="btn btn-secondary">선택 리비젼 비교</button>
${navbtns}
`;
for(var row of data) {
content += `
-
${generateTime(toDate(row.time), timeFormat)}
(<a rel=nofollow href="/w/${encodeURIComponent(title)}?rev=${row.rev}">보기</a> |
<a rel=nofollow href="/raw/${encodeURIComponent(title)}?rev=${row.rev}" data-npjax="true">RAW</a> |
<a rel=nofollow href="/blame/${encodeURIComponent(title)}?rev=${row.rev}">Blame</a> |
<a rel=nofollow href="/revert/${encodeURIComponent(title)}?rev=${row.advance == 'revert' ? Number(row.flags) : row.rev}">이 리비젼으로 되돌리기</a>${
Number(row.rev) > 1
? ' | <a rel=nofollow href="/diff/' + encodeURIComponent(title) + '?rev=' + row.rev + '&oldrev=' + String(Number(row.rev) - 1) + '">비교</a>'
:
})
<input type="radio" name="oldrev" value="${row.rev}">
<input type="radio" name="rev" value="${row.rev}">
${row.advance != 'normal' ? `(${edittype(row.advance, ...(row.flags.split('\n')))})` : }
r${row.rev}
( 0
? 'green'
: (
Number(row.changes) < 0
? 'red'
: 'gray'
)
)
};">${row.changes})
${row.edit_request_id ? '<a href="/edit_request/' + row.edit_request_id + '">(편집 요청)</a>' : } ${ip_pas(row.username, row.ismember)}
(${row.log})
`;
}
content += `
${navbtns}
<script>historyInit("${encodeURIComponent(title)}");</script>
`;
res.send(await render(req, totitle(doc.title, doc.namespace) + '의 역사', content, {
document: doc,
}, , null, 'history'));
});
wiki.get(/^\/discuss\/(.*)/, async function threadList(req, res) {
const title = req.params[0];
const doc = processTitle(title);
var state = req.query['state'];
if(!state) state = ;
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var content = ;
var trdlst;
var subtitle = ;
var viewname = ;
if(state == 'close') {
content += '
';
var cnt = 0;
trdlst = await curs.execute("select topic, tnum from threads where title = ? and namespace = ? and status = 'close' and not deleted = '1' order by cast(time as integer) desc", [doc.title, doc.namespace]);
for(var trd of trdlst) {
content += `- ${++cnt}. <a href="/thread/${trd.tnum}">${html.escape(trd.topic)}</a>
`;
}
content += '
';
subtitle = ' (닫힌 토론)';
viewname = 'thread_list_close';
} else if(state == 'closed_edit_requests') {
content += '
';
trdlst = await curs.execute("select id from edit_requests where state = 'closed' and not deleted = '1' and title = ? and namespace = ? order by cast(date as integer) desc", [doc.title, doc.namespace]);
for(var trd of trdlst) {
content += `- <a href="/edit_request/${trd.id}">편집 요청 ${trd.id}</a>
`;
}
content += '
';
subtitle = ' (닫힌 편집 요청)';
viewname = 'edit_request_list_close';
} else {
/*
{
document: { namespace: '나무위키', title: '대문' },
thread_list: [
{
discuss: [Array],
slug: 'AFantasticAndTestyNoise',
topic: '토론 생성 전 꼭 확인 바랍니다.'
}
],
editRequests: [],
captcha: true,
deleteThread: false,
body: {}
}
*/
content += `
편집 요청
`;
var editRequests = [];
var captcha = false;
var deleteThread = !!getperm('delete_thread', ip_check(req));
trdlst = await curs.execute("select id from edit_requests where state = 'open' and not deleted = '1' and title = ? and namespace = ? order by cast(date as integer) desc", [doc.title, doc.namespace]);
for(var item of trdlst) {
content += `- <a href="/edit_request/${item.id}">편집 요청 ${item.id}</a>
`;
}
content += `
<a href="?state=closed_edit_requests">[닫힌 편집 요청 보기]</a>
`;
content += `
토론
`;
var cnt = 0;
trdlst = await curs.execute("select topic, tnum from threads where title = ? and namespace = ? and not status = 'close' and not deleted = '1' order by cast(time as integer) desc", [doc.title, doc.namespace]);
for(var trd of trdlst) {
content += `- <a href="#${++cnt}">${cnt}</a>. <a href="/thread/${trd.tnum}">${html.escape(trd.topic)}</a>
`;
}
content += `
<a href="?state=close">[닫힌 토론 목록 보기]</a>
`
cnt = 0;
var thread_list = [];
for(var trd of trdlst) {
content += `
${cnt}. <a href="/thread/${trd.tnum}">${html.escape(trd.topic)}</a>
`;
const d = {
slug: trd.tnum,
topic: trd.topic,
discuss: [],
};
const td = await curs.execute("select isadmin, id, content, username, time, hidden, hider, status, ismember from res where tnum = ? order by cast(id as integer) asc", [trd.tnum]);
const ltid = Number((await curs.execute("select id from res where tnum = ? order by cast(id as integer) desc limit 1", [trd.tnum]))[0]['id']);
var ambx = false;
const fstusr = (await curs.execute("select username from res where tnum = ? and (id = '1')", [trd.tnum]))[0]['username'];
for(var rs of td) {
const crid = Number(rs['id']);
if(ltid > 4 && crid != 1 && (crid < ltid - 2)) {
if(!ambx) {
content += `
<a class=more-box href="/thread/${trd.tnum}">more...</a>
`;
ambx = true;
}
continue;
}
content += `
#${rs['id']} ${ip_pas(rs['username'], rs['ismember'], 1).replace('<a ', rs.isadmin == '1' ? '<a style="font-weight: bold;" ' : '<a ')} ${generateTime(toDate(rs['time']), timeFormat)}
${
rs['hidden'] == '1'
? (
getperm('hide_thread_comment', ip_check(req))
? '[' + rs['hider'] + '에 의해 숨겨진 글입니다.]<a class="text" onclick="$(this).parent().parent().children(\'.hidden-content\').show(); $(this).parent().css(\'margin\', \'15px 0 15px -10px\'); return false;" style="display: block; color: #fff;">[ADMIN] Show hidden content</a> '
: '[' + rs['hider'] + '에 의해 숨겨진 글입니다.]'
)
: await markdown(rs['content'], 1)
}
`;
const t = {
id: rs.id,
text: rs.content,
date: Math.floor(Number(rs.time / 1000)),
hide_author: rs.hidden == '1' ? rs.hider : null,
type: rs.status == '1' ? 'status' : 'normal',
admin: rs.isadmin == '1' ? true : false };
t[rs.ismember] = rs.username;
d.discuss.push(t);
}
content += '
';
thread_list.push(d);
}
content += `
새 주제 생성
${doc + == (config.getString('wiki.front_page', 'FrontPage')) ? `
[경고!] 이 토론은 ${doc + } 문서의 토론입니다. ${doc + } 문서와 관련 없는 토론은 각 문서의 토론에서 진행해 주시기 바랍니다. ${doc + } 문서와 관련 없는 토론은 삭제될 수 있습니다.
` : }
<form method=post class="new-thread-form" id="topicForm">
<input type="hidden" name="identifier" value="${islogin(req) ? 'm' : 'i'}:${ip_check(req)}">
<label class=control-label for="topicInput" style="margin-bottom: 0.2rem;">주제 :</label>
<input type="text" class=form-control id="topicInput" name="topic">
<label class=control-label for="contentInput" style="margin-bottom: 0.2rem;">내용 :</label>
<textarea name="text" class=form-control id="contentInput" rows="5"></textarea>
${islogin(req) ? : `
[알림] 비로그인 상태로 토론 주제를 생성합니다. 토론 내역에 IP(${ip_check(req)})가 영구히 기록됩니다.
`}
<button id="createBtn" class="btn btn-primary" style="width: 8rem;">전송</button>
</form>
`;
subtitle = ' (토론)';
viewname = 'thread_list';
}
res.send(await render(req, totitle(doc.title, doc.namespace) + subtitle, content, {
document: doc,
deleteThread,
captcha,
thread_list,
editRequests,
}, , null, viewname));
});
wiki.post(/^\/discuss\/(.*)/, async function createThread(req, res) {
const title = req.params[0];
const doc = processTitle(title);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var aclmsg = await getacl(req, doc.title, doc.namespace, 'create_thread', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_create_thread', msg: aclmsg }));
if(!req.body['topic']) return res.send(await showError(req, { code: 'validator_required', tag: 'topic' }));
if(!req.body['text']) return res.send(await showError(req, { code: 'validator_required', tag: 'text' }));
var tnum;
do {
tnum = rndval('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 22);
var dd = await curs.execute("select tnum from threads where tnum = ?", [tnum]);
if(!dd.length) break;
} while(1);
const newid = newID();
await curs.execute("insert into threads (title, namespace, topic, status, time, tnum, slug) values (?, ?, ?, ?, ?, ?, ?)",
[doc.title, doc.namespace, req.body['topic'], 'normal', getTime(), tnum, newid]);
await curs.execute("insert into res (id, content, username, time, hidden, hider, status, tnum, ismember, isadmin, slug) values \
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
['1', req.body['text'], ip_check(req), getTime(), '0', , '0', tnum, islogin(req) ? 'author' : 'ip', getperm('admin', ip_check(req)) ? '1' : '0', newid]);
res.redirect('/thread/' + tnum);
});
wiki.get(/^\/topic\/(\d+)$/, async(req, res, next) => {
const num = req.params[0];
var data = await curs.execute("select tnum from threads where num = ?", [num]);
if(data.length) return res.redirect('/thread/' + data[0].tnum);
next();
});
/* if(minor >= 16) wiki.get(/^\/thread\/([a-zA-Z0-9]{18,24})$/, async(req, res, next) => {
const tnum = req.params[0];
var data = await curs.execute("select slug, tnum from threads where tnum = ?", [tnum]);
if(data.length && tnum != data[0].slug) return res.redirect('/thread/' + data[0].slug);
next();
}); */
wiki.get(minor >= 16 ? /^\/thread\/([a-zA-Z0-9]+)$/ : /^\/thread\/([a-zA-Z0-9]{18,24})$/, async function viewThread(req, res) {
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
var data = await curs.execute("select title, namespace, topic, status, slug from threads where tnum = ?", [tnum]);
const { title, topic, status, namespace } = data[0];
const doc = totitle(title, namespace);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var content = `
${html.escape(topic)}
${
getperm('delete_thread', ip_check(req))
? '<a onclick="return confirm(\'삭제하시겠습니까?\');" href="/admin/thread/' + tnum + '/delete" class="btn btn-danger btn-sm">[ADMIN] 삭제</a>'
:
}
`;
for(var i=1; i<=rescount; i++) {
content += `
<a id="${i}">#${i}</a>
`;
}
content += `
<script>$(function() { discussPollStart("${tnum}"); });</script>
댓글 달기
`;
if(getperm('update_thread_status', ip_check(req))) {
var sts = ;
if(status == 'close')
sts = `
<option value=normal>normal</option>
<option value=pause>pause</option>
`;
if(status == 'normal')
sts = `
<option value=close>close</option>
<option value=pause>pause</option>
`;
if(status == 'pause')
sts = `
<option value=close>close</option>
<option value=normal>normal</option>
`;
content += `
<form method=post id=thread-status-form>
[ADMIN] 쓰레드 상태 변경
<select name=status>${sts}</select>
<button id=changeBtn class="d_btn type_blue">변경</button>
</form>
`;
}
if(getperm('update_thread_document', ip_check(req)) && (minor >= 5 || (minor == 4 && revision >= 3))) {
content += `
<form method=post id=thread-document-form>
[ADMIN] 쓰레드 이동
<input type=text name=document value="${doc}">
<button id=changeBtn class="d_btn type_blue">변경</button>
</form>
`;
}
if(getperm('update_thread_topic', ip_check(req)) && (minor >= 5 || (minor == 4 && revision >= 3))) {
content += `
<form method=post id=thread-topic-form>
[ADMIN] 쓰레드 주제 변경
<input type=text name=topic value="${topic}">
<button id=changeBtn class="d_btn type_blue">변경</button>
</form>
`;
}
content += `
<form id=new-thread-form method=post>
<textarea class=form-control${['close', 'pause'].includes(status) ? ' readonly disabled' : } rows=5 name=text>${status == 'pause' ? 'pause 상태입니다.' : (status == 'close' ? '닫힌 토론입니다.' : )}</textarea>
${islogin(req) ? : `
[알림] 비로그인 상태로 토론에 참여합니다. 토론 내역에 IP(${ip_check(req)})가 영구히 기록됩니다.
`}
<button type=submit class="btn btn-primary" style="width: 120px;"${['close', 'pause'].includes(status) ? ' disabled' : }>전송</button>
</form>
`;
res.send(await render(req, totitle(title, namespace) + ' (토론) - ' + topic, content, {
document: doc,
}, , null, 'thread'));
});
wiki.post(/^\/thread\/([a-zA-Z0-9]{18,24})$/, async function postThreadComment(req, res) {
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
var data = await curs.execute("select title, namespace, topic, status, slug from threads where tnum = ?", [tnum]);
const { title, topic, status, namespace } = data[0];
const doc = totitle(title, namespace);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
var aclmsg = await getacl(req, doc.title, doc.namespace, 'write_thread_comment', 1);
if(aclmsg) return res.status(403).json({ status: aclmsg });
if(['close', 'pause'].includes(status)) return res.status(403).json({});
if(!req.body['text']) return res.status(400).json({ status: err('error', { code: 'validator_required', tag: 'text' }) + });
var data = await curs.execute("select id from res where tnum = ? order by cast(id as integer) desc limit 1", [tnum]);
const lid = Number(data[0].id);
await curs.execute("insert into res (id, content, username, time, hidden, hider, status, tnum, ismember, isadmin) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
String(lid + 1), req.body['text'], ip_check(req), getTime(), '0', , '0', tnum, islogin(req) ? 'author' : 'ip', getperm('admin', ip_check(req)) ? '1' : '0'
]);
await curs.execute("update threads set time = ? where tnum = ?", [getTime(), tnum]);
res.json({});
});
wiki.get(/^\/thread\/([a-zA-Z0-9]{18,24})\/(\d+)$/, async function sendThreadData(req, res) {
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
const tid = req.params[1];
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
var data = await curs.execute("select username from res where tnum = ? and (id = '1')", [tnum]);
const fstusr = data[0]['username'];
var data = await curs.execute("select title, namespace, topic, status, slug from threads where tnum = ?", [tnum]);
const { title, topic, status, namespace } = data[0];
const doc = totitle(title, namespace);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'read', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
content = ``;
var data = await curs.execute("select isadmin, type, id, content, username, time, hidden, hider, status, ismember from res where tnum = ? and (cast(id as integer) = 1 or (cast(id as integer) >= ? and cast(id as integer) < ?)) order by cast(id as integer) asc", [tnum, Number(tid), Number(tid) + 30]);
for(var rs of data) {
var rescontent = rs.status == 1
? (
rs.type == 'status'
? ((minor >= 5 || (minor == 4 && revision >= 3)) ? ('스레드 상태를 ' + rs.content + '로 변경') : ('토픽 상태를 ' + rs.content + '로 변경'))
: (
rs.type == 'document'
? '스레드를 ' + rs.content + ' 문서로 이동'
: '스레드 주제를 ' + rs.content + '로 변경'
)
) : await markdown(rs.content, 1);
if(rs.hidden == '1') {
var rc = rescontent;
rescontent = '[' + rs.hider + '에 의해 숨겨진 글입니다.]';
if(getperm('hide_thread_comment', ip_check(req))) {
if(minor >= 13) {
rescontent += '<a class="btn btn-danger" onclick="$(this).parent().attr(\'class\', \'r-body\'); $(this).parent().html($(this).parent().children(\'.hidden-content\').html()); return false;">[ADMIN] SHOW</a>
';
} else {
rescontent += '
<a class=text onclick="$(this).parent().parent().children(\'.hidden-content\').show(); $(this).parent().css(\'margin\', \'15px 0 15px -10px\'); $(this).hide(); return false;" style="display: block; color: #fff;">[ADMIN] Show hidden content</a>
';
}
}
}
content += `
${rescontent}
`;
if(getperm('hide_thread_comment', ip_check(req))) {
content += `
`;
}
content += `
`;
}
res.send(content);
});
wiki.get(/^\/admin\/thread\/([a-zA-Z0-9]{18,24})\/(\d+)\/show$/, async function showHiddenComment(req, res) {
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
const tid = req.params[1];
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
if(!getperm('hide_thread_comment', ip_check(req))) return res.send(await showError(req, 'permission'));
await curs.execute("update res set hidden = '0', hider = where tnum = ? and id = ?", [tnum, tid]);
res.redirect('/thread/' + tnum);
});
wiki.get(/^\/admin\/thread\/([a-zA-Z0-9]{18,24})\/(\d+)\/hide$/, async function hideComment(req, res) {
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
const tid = req.params[1];
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
if(!getperm('hide_thread_comment', ip_check(req))) return res.send(await showError(req, 'permission'));
await curs.execute("update res set hidden = '1', hider = ? where tnum = ? and id = ?", [ip_check(req), tnum, tid]);
res.redirect('/thread/' + tnum);
});
wiki.post(/^\/admin\/thread\/([a-zA-Z0-9]{18,24})\/status$/, async function updateThreadStatus(req, res) {
if(!getperm('update_thread_status', ip_check(req))) return res.status(403).send(await showError(req, 'permission'));
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
var newstatus = req.body['status'];
if(!['close', 'pause', 'normal'].includes(newstatus)) res.status(400).send();
await curs.execute("update threads set time = ?, status = ? where tnum = ?", [getTime(), newstatus, tnum]);
await curs.execute("insert into res (id, content, username, time, hidden, hider, status, tnum, ismember, isadmin, type) \
values (?, ?, ?, ?, '0', , '1', ?, ?, ?, 'status')", [
String(rescount + 1), newstatus, ip_check(req), getTime(), tnum, islogin(req) ? 'author' : 'ip', getperm('admin', ip_check(req)) ? '1' : '0'
]);
res.json({});
});
wiki.post(/^\/admin\/thread\/([a-zA-Z0-9]{18,24})\/document$/, async function updateThreadDocument(req, res) {
if(!getperm('update_thread_document', ip_check(req))) return res.status(403).send(await showError(req, 'permission'));
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
var newdoc = req.body['document'];
if(!newdoc.length) return res.status(400).send();
var dd = processTitle(newdoc);
var aclmsg = await getacl(req, dd.title, dd.namespace, 'create_thread', 1);
if(aclmsg) return res.json({
status: aclmsg,
});
await curs.execute("update threads set time = ?, title = ?, namespace = ? where tnum = ?", [getTime(), dd.title, dd.namespace, tnum]);
await curs.execute("insert into res (id, content, username, time, hidden, hider, status, tnum, ismember, isadmin, type) \
values (?, ?, ?, ?, '0', , '1', ?, ?, ?, 'document')", [
String(rescount + 1), newdoc, ip_check(req), getTime(), tnum, islogin(req) ? 'author' : 'ip', getperm('admin', ip_check(req)) ? '1' : '0'
]);
res.json({});
});
wiki.post(/^\/admin\/thread\/([a-zA-Z0-9]{18,24})\/topic$/, async function updateThreadTopic(req, res) {
if(!getperm('update_thread_topic', ip_check(req))) return res.status(403).send(await showError(req, 'permission'));
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
var rescount = data.length;
var data = await curs.execute("select deleted from threads where tnum = ?", [tnum]);
if(data.length && data[0].deleted == '1') rescount = 0;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
var newtopic = req.body['topic'];
if(!newtopic.length) return res.status(400).send();
await curs.execute("update threads set time = ?, topic = ? where tnum = ?", [getTime(), newtopic, tnum]);
await curs.execute("insert into res (id, content, username, time, hidden, hider, status, tnum, ismember, isadmin, type) \
values (?, ?, ?, ?, '0', , '1', ?, ?, ?, 'topic')", [
String(rescount + 1), newtopic, ip_check(req), getTime(), tnum, islogin(req) ? 'author' : 'ip', getperm('admin', ip_check(req)) ? '1' : '0'
]);
res.json({});
});
wiki.get(/^\/admin\/thread\/([a-zA-Z0-9]{18,24})\/delete/, async function deleteThread(req, res) {
if(!getperm('delete_thread', ip_check(req))) return res.send(await showError(req, 'permission'));
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
var data = await curs.execute("select id from res where tnum = ?", [tnum]);
const rescount = data.length;
if(!rescount) return res.send(await showError(req, 'thread_not_found'));
var data = await curs.execute("select title, namespace from threads where tnum = ?", [tnum]);
const title = totitle(data[0].title, data[0].namespace) + ;
await curs.execute("update threads set deleted = '1' where tnum = ?", [tnum]);
res.redirect('/discuss/' + encodeURIComponent(title));
});
wiki.post(/^\/notify\/thread\/([a-zA-Z0-9]{18,24})$/, async function notifyEvent(req, res) {
var tnum = req.params[0];
var slug = tnum;
var data = await curs.execute("select tnum from threads where slug = ?", [tnum]);
if(data.length) tnum = data[0].tnum;
var dd = await curs.execute("select id from res where tnum = ?", [tnum]);
const rescount = dd.length;
if(!rescount) return res.send(await showError(req, "thread_not_found"));
var data = await curs.execute("select id from res where tnum = ? order by cast(time as integer) desc limit 1", [tnum]);
res.json({
status: 'event',
comment_id: Number(data[0].id),
});
});
wiki.all(/^\/delete\/(.*)/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
const title = req.params[0];
const doc = processTitle(title);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2);
if(aclmsg) return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg }));
var aclmsg = await getacl(req, doc.title, doc.namespace, 'delete', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_delete', msg: aclmsg }));
const o_o = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
if(!o_o.length) return res.send(await showError(req, 'document_not_found'));
var content = `
<form id=deleteForm method=post>
<label class=control-label for=logInput>요약</label>
<input type=text id=logInput name=log class=form-control />
<label>
<label><input type=checkbox name=agree id=agreeCheckbox value=Y /> 문서 이동 목적이 아닌, 삭제하기 위함을 확인합니다.</label>
</label>
알림! : 문서의 제목을 변경하려는 경우 <a href="/move/${encodeURIComponent(doc + )}">문서 이동</a> 기능을 사용해주세요. 문서 이동 기능을 사용할 수 없는 경우 토론 기능이나 게시판을 통해 대행 요청을 해주세요.
<button type=reset class="btn btn-secondary">초기화</button>
<button type=submit class="btn btn-primary" id=submitBtn>삭제</button>
</form>
`;
var error = null;
if(req.method == 'POST') do {
if(doc.namespace == '사용자')
if((minor >= 11 && !doc.title.includes('/')) || minor < 11) {
content = (error = err('alert', 'disable_user_document')) + content;
break;
}
if(!req.body['agree']) {
content = (error = err('alert', 'validator_required', 'agree')) + content;
break;
}
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]);
const recentRev = _recentRev[0];
await curs.execute("delete from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
const rawChanges = 0 - recentRev.content.length;
curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
doc.title, doc.namespace, , String(Number(recentRev.rev) + 1), ip_check(req), getTime(), + (rawChanges), req.body['log'] || , '0', '-1', islogin(req) ? 'author' : 'ip', 'delete'
]);
curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]);
return res.redirect('/w/' + encodeURIComponent(doc + ));
} while(0);
res.send(await render(req, doc + ' (삭제)', content, {
document: doc,
}, , null, 'delete'));
});
wiki.all(/^\/move\/(.*)/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
const title = req.params[0];
const doc = processTitle(title);
var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 2);
if(aclmsg) return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg }));
var aclmsg = await getacl(req, doc.title, doc.namespace, 'move', 1);
if(aclmsg) return res.send(await showError(req, { code: 'permission_move', msg: aclmsg }));
const o_o = await curs.execute("select title from history where title = ? and namespace = ?", [doc.title, doc.namespace]);
if(!o_o.length) return res.send(await showError(req, 'document_not_found'));
// 원래 이랬나...?
var content = `
<form method=post id=moveForm>
<label>변경할 문서 제목 : </label>
<input name=title type=text style="width: 250px;" id=titleInput />
<label>요약 : </label>
<input style="width: 600px;" name=log type=text id=logInput />
<label>문서를 서로 맞바꾸기 : </label>
<input type=checkbox name=mode value=swap />
<button type=submit>이동</button>
</form>
`;
var error = null;
if(req.method == 'POST') do {
if(doc.namespace == '사용자')
if((minor >= 11 && !doc.title.includes('/')) || minor < 11) {
content = (error = err('alert', 'disable_user_document')) + content;
break;
}
var doccontent = ;
const o_o = await curs.execute("select content from documents where title = ? and namespace = ?", [doc.title, doc.namespace]);
if(o_o.length) {
doccontent = o_o[0].content;
}
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]);
const recentRev = _recentRev[0];
if(!req.body['title']) {
content = (error = err('alert', { code: 'validator_required', tag: 'title' })) + content;
break;
}
const newdoc = processTitle(req.body['title']);
var aclmsg = await getacl(req, newdoc.title, newdoc.namespace, 'read', 1);
if(aclmsg) {
return res.send(await showError(req, { code: 'permission_read', msg: aclmsg }));
}
var aclmsg = await getacl(req, newdoc.title, newdoc.namespace, 'edit', 2);
if(aclmsg) {
return res.send(await showError(req, { code: 'permission_edit', msg: aclmsg }));
}
if(req.body['mode'] == 'swap') {
return res.send(await showError(req, 'feature_not_implemented'));
} else {
const d_d = await curs.execute("select rev from history where title = ? and namespace = ?", [newdoc.title, newdoc.namespace]);
if(d_d.length) {
return res.send(await showError(req, '문서가 이미 존재합니다.', 1));
}
await curs.execute("update documents set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]);
await curs.execute("update acl set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]);
curs.execute("update threads set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]);
curs.execute("update edit_requests set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]);
curs.execute("update history set title = ?, namespace = ? where title = ? and namespace = ?", [newdoc.title, newdoc.namespace, doc.title, doc.namespace]);
}
curs.execute("insert into history (title, namespace, content, rev, username, time, changes, log, iserq, erqnum, ismember, advance, flags) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
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
]);
curs.execute("update documents set time = ? where title = ? and namespace = ?", [doc.title, doc.namespace]);
return res.redirect('/w/' + encodeURIComponent(newdoc + ));
} while(0);
res.send(await render(req, doc + ' (이동)', content, {
document: doc,
}, , error, 'move'));
});
if(minor < 18) wiki.all(/^\/admin\/suspend_account$/, async(req, res) => {
if(!['POST', 'GET'].includes(req.method)) return next();
if(!hasperm(req, 'suspend_account')) return res.status(403).send(await showError(req, 'permission'));
var content = `
<form method=post>
<label>유저 이름 : </label>
<input class=form-control id=usernameInput name=username style="width: 250px;" value="${req.method == 'POST' ? html.escape(req.body['username'] || ) : }" type=text />
<label>메모 : </label>
<input class=form-control id=noteInput name=note style="width: 400px;" value="${req.method == 'POST' ? html.escape(req.body['note'] || ) : }" type=text />
<label>기간 : </label>
<select class=form-control name=expire id=expireSelect>
${expireopt(req)}
</select>
<button class="btn btn-info pull-right" id=moveBtn style="width: 100px;" type=submit>확인</button>
</form>
`;
var error = null;
if(req.method == 'POST') do {
var { expire, note, username } = req.body;
if(!username) { content = (error = err('alert', { code: 'validator_required', tag: 'username' })) + content; break; }
if((hostconfig.owners || []).includes(username)) { content = (error = err('alert', { code: 'invalid_permission' })) + content; break; }
var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]);
if(!data.length) { content = (error = err('alert', { code: 'invalid_username' })) + content; break; }
username = data[0].username;
if(!note) { content = (error = err('alert', { code: 'validator_required', tag: 'note' })) + content; break; }
if(!expire) { content = (error = err('alert', { code: 'validator_required', tag: 'expire' })) + content; break; }
if(isNaN(Number(expire))) { content = (error = err('alert', { code: 'invalid_type_number', tag: 'expire' })) + content; break; }
if(Number(expire) > 29030400) { content = (error = err('alert', { msg: 'expire의 값은 29030400 이하이어야 합니다.' })) + content; break; }
if(expire == '-1') {
if(!(await userblocked(username))) { content = (error = err('alert', { code: 'already_unsuspend_account' })) + content; break; }
curs.execute("delete from suspend_account where username = ?", [username]);
var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(data.length) logid = Number(data[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'suspend_account',
duration: '-1',
note,
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
target: username,
logid,
});
return res.redirect('/admin/suspend_account');
}
if(await userblocked(username)) { content = (error = err('alert', { code: 'already_suspend_account' })) + content; break; }
const date = getTime();
const expiration = expire == '0' ? '0' : String(Number(date) + Number(expire) * 1000);
curs.execute("insert into suspend_account (username, date, expiration, note) values (?, ?, ?, ?)", [username, String(getTime()), expiration, note]);
var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(data.length) logid = Number(data[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'suspend_account',
duration: expire,
note,
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
target: username,
logid,
});
return res.redirect('/admin/suspend_account');
} while(0);
return res.send(await render(req, '사용자 차단', content, {}, , error, 'suspend_account'));
});
wiki.all(/^\/admin\/grant$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
var username = req.query['username'];
if(!getperm('grant', ip_check(req))) return res.send(await showError(req, 'permission'));
var error = null;
var content = `
<form method=get>
<label>유저 이름 :</label>
<input type=text id=usernameInput class=form-control style="width: 250px;" name=username value="${html.escape(username ? username : )}" />
<button type=submit class="btn btn-info pull-right" style="width: 100px;">확인</button>
</form>
`;
if(username === undefined) return res.send(await render(req, '권한 부여', content, {}, _, error, 'grant'));
if(!username) return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'validator_required', tag: 'username' })) + content, {}, _, error, 'grant'));
var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]);
if(!data.length)
return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'invalid_username' })) + content, {}, _, error, 'grant'));
username = data[0].username;
var chkbxs = ;
for(var prm of perms) {
// if(!getperm('developer', ip_check(req), 1) && 'developer' == (prm)) continue;
chkbxs += `
${prm} <input type=checkbox ${getperm(prm, username, 1) ? 'checked' : } name=permissions value="${prm}" />
`;
}
content += `
사용자 ${html.escape(username)}
<form method=post>
${chkbxs}
<button type=submit class="btn btn-info pull-right" style="width: 100px;">확인</button>
</form>
`;
if(req.method == 'POST') {
if(!username) return res.send(await showError(req, 'invalid_username'));
var data = await curs.execute("select username from users where username = ?", [username]);
if(!data.length) return res.send(await showError(req, 'invalid_username'));
var prmval = req.body['permissions'];
if(!prmval || !prmval.find) prmval = [prmval];
var logstring = ;
for(var prm of perms) {
// if(!getperm('developer', ip_check(req), 1) && 'developer' == (prm)) continue;
if(getperm(prm, username, 1) && (typeof(prmval.find(item => item == prm)) == 'undefined')) {
logstring += '-' + prm + ' ';
if(permlist[username]) permlist[username].splice(permlist[username].findIndex(item => item == prm), 1);
curs.execute("delete from perms where perm = ? and username = ?", [prm, username]);
} else if(!getperm(prm, username, 1) && (typeof(prmval.find(item => item == prm)) != 'undefined')) {
logstring += '+' + prm + ' ';
if(!permlist[username]) permlist[username] = [prm];
else permlist[username].push(prm);
curs.execute("insert into perms (perm, username) values (?, ?)", [prm, username]);
}
}
if(!logstring.length)
return res.send(await render(req, '권한 부여', (error = err('alert', { code: 'no_change' })) + content, {}, _, error, 'grant'));
var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(data.length) logid = Number(data[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'grant',
note: logstring,
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
target: username,
logid,
});
return res.redirect('/admin/grant?username=' + encodeURIComponent(username));
}
res.send(await render(req, '권한 부여', content, {}, _, _, 'grant'));
});
wiki.all(/^\/admin\/login_history$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
if(!getperm('grant', ip_check(req))) return res.send(await showError(req, 'permission'));
var error = null;
var content = `
<form method=post>
<label>유저 이름 :</label>
<input type=text id=usernameInput class=form-control style="width: 250px;" name=username />
<button type=submit class="btn btn-info pull-right" style="width: 100px;">확인</button>
</form>
`;
if(req.method == 'POST') {
var username = req.body['username'];
if(!username) return res.send(await render(req, '로그인 내역', (error = err('alert', { code: 'validator_required', tag: 'username' })) + content, {}, _, error, 'login_history'));
var data = await curs.execute("select username from users where lower(username) = ?", [username.toLowerCase()]);
if(!data.length)
return res.send(await render(req, '로그인 내역', (error = err('alert', { code: 'invalid_username' })) + content, {}, _, error, 'login_history'));
username = data[0].username;
const id = rndval('abcdef1234567890', 64);
if(!loginHistory[ip_check(req)]) loginHistory[ip_check(req)] = {};
var history = await curs.execute("select ip, time from login_history where username = ? order by cast(time as integer) desc limit 50", [username]);
var ua = await curs.execute("select string from useragents where username = ?", [username]);
loginHistory[ip_check(req)][id] = { username, useragent: (ua[0] || { string: }).string, history };
var logid = 1, lgdata = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(lgdata.length) logid = Number(lgdata[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'login_history',
duration: 0,
note: ,
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
target: username,
logid,
});
return res.redirect('/admin/login_history/' + id);
}
return res.send(await render(req, '로그인 내역', content, {}, _, _, 'login_history'));
});
wiki.get(/^\/admin\/login_history\/(.+)$/, async(req, res) => {
const id = req.params[0];
if(!loginHistory[ip_check(req)] || (loginHistory[ip_check(req)] && !loginHistory[ip_check(req)][id]))
return res.redirect('/admin/login_history');
const { username, history, useragent } = loginHistory[ip_check(req)][id];
var content = `
마지막 로그인 UA : ${html.escape(useragent)}
이메일 : ${getUserSetting(username, 'email', )}
${navbtn(0, 0, 0, 0)}
<tbody>
`;
for(var item of history) {
content += ``;
}
content += `
</tbody>
Date
IP
${generateTime(toDate(item.time), timeFormat)} ${item.ip}
${navbtn(0, 0, 0, 0)}
`;
return res.send(await render(req, username + ' 로그인 내역', content, {}, _, _, 'login_history'));
});
if(minor < 18) wiki.post(/^\/admin\/ipacl\/remove$/, async(req, res) => {
if(!hasperm(req, 'ipacl')) return res.status(403).send(await showError(req, 'permission'));
if(!req.body['ip']) return res.status(400).send(await showError(req, { code: 'validator_required', tag: 'ip' }));
var dbdata = await curs.execute("select cidr from ipacl where cidr = ?", [req.body['ip']]);
if(!dbdata.length) return res.status(400).send(await showError(req, 'invalid_value'));
await curs.execute("delete from ipacl where cidr = ?", [req.body['ip']]);
var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(data.length) logid = Number(data[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'ipacl_remove',
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
target: req.body['ip'],
logid,
});
return res.redirect('/admin/ipacl');
});
if(minor < 18) wiki.all(/^\/admin\/ipacl$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
if(!hasperm(req, 'ipacl')) return res.status(403).send(await showError(req, 'permission'));
const { from, until } = req.query;
var error = null;
await curs.execute("delete from ipacl where not expiration = '0' and ? > cast(expiration as integer)", [Number(getTime())]);
var ld = await curs.execute("select cidr from ipacl order by cidr desc limit 1");
var fd = await curs.execute("select cidr from ipacl order by cidr asc limit 1");
var data = await curs.execute("select cidr, al, expiration, note, date from ipacl " + (from ? "where cidr > ?" : (until ? "where cidr < ?" : "")) + " order by cidr " + (until ? 'desc' : 'asc') + " limit 50", (from || until ? [from || until] : []));
if(until) data = data.reverse();
try {
var navbtns = navbtnss(fd[0].cidr, ld[0].cidr, data[0].cidr, data[data.length-1].cidr, '/admin/ipacl');
} catch(e) {
var navbtns = navbtn(0, 0, 0, 0);
}
var content = `
<form method=post class=settings-section>
<label class=control-label>IP 주소 (CIDR<a href="https://ko.wikipedia.org/wiki/%EC%82%AC%EC%9D%B4%EB%8D%94_(%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9)" target=_blank>[?]</a>) :</label>
<input type=text class=form-control id=ipInput name=ip value="${req.method == 'POST' ? html.escape(req.body['ip'] || ) : }" />
<label class=control-label>메모 :</label>
<input type=text class=form-control id=noteInput name=note value="${req.method == 'POST' ? html.escape(req.body['note'] || ) : }" />
<label class=control-label>차단 기간 :</label>
<select class=form-control name=expire>
${expireopt(req)}
</select>
<label class=control-label>로그인 허용 :</label>
<label>
<input type=checkbox id=allowLoginInput name=allow_login${req.method == 'POST' ? (req.body['allow_login'] ? ' checked' : ) : } /> Yes
</label>
<button type=submit class="btn btn-primary" style="width: 90px;">추가</button>
</form>
${navbtns}
<form class="form-inline pull-right" id=searchForm method=get>
<input type=text class=form-control id=searchQuery name=from placeholder="CIDR" />
<button type=submit class="btn btn-primary">Go</button>
</form>
<colgroup>
<col style="width: 150px;" />
<col />
<col style="width: 200px" />
<col style="width: 160px" />
<col style="width: 60px" />
<col style="width: 60px;" />
</colgroup>
<thead>
</thead>
<tbody>
`;
for(var row of data) {
content += `
`;
}
content += `
</tbody>
IP
메모
차단일
만료일
AL
작업
${row.cidr}
${row.note}
${generateTime(toDate(row.date), timeFormat)}
${!Number(row.expiration) ? '영구' : generateTime(toDate(row.expiration), timeFormat)}
${row.al == '1' ? 'Y' : 'N'}
<form method=post onsubmit="return confirm('정말로?');" action="/admin/ipacl/remove">
<input type=hidden name=ip value="${row.cidr}" />
<input type=submit class="btn btn-sm btn-danger" value="삭제" />
</form>
AL = Allow Login(로그인 허용)
`;
var error = false;
if(req.method == 'POST') {
var { ip, allow_login, expire, note } = req.body;
for(var val of ['ip', 'note', 'expire']) {
if(!req.body[val]) return res.send(await render(req, 'IPACL', (error = err('alert', { code: 'validator_required', tag: val })) + content, {}, , error, 'ipacl'));
}
if(!ip.includes('/')) ip += '/32';
if(!ip.match(/^([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\/([1-9]|[12][0-9]|3[0-2])$/)) error = true, content = alertBalloon(fetchErrorString('invalid_cidr'), 'danger', true, 'fade in') + content;
else {
const date = getTime();
if(isNaN(Number(expire))) {
return res.send(await render(req, 'IPACL', (error = err('alert', { code: 'invalid_type_number', tag: 'expire' })) + content, {}, , error, 'ipacl'));
}
if(Number(expire) > 29030400) {
return res.send(await render(req, 'IPACL', (error = err('alert', { msg: 'expire의 값은 29030400 이하이어야 합니다.' })) + content, {}, , error, 'ipacl'));
}
const expiration = expire == '0' ? '0' : String(Number(date) + Number(expire) * 1000);
var data = await curs.execute("select cidr from ipacl where cidr = ? limit 1", [ip]);
if(data.length) content = (error = err('alert', { code: 'ipacl_already_exists' })) + content;
else {
await curs.execute("insert into ipacl (cidr, al, expiration, note, date) values (?, ?, ?, ?, ?)", [ip, allow_login ? '1' : '0', expiration, note, date]);
var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(data.length) logid = Number(data[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'ipacl_add',
duration: expire,
note,
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
target: ip,
logid,
});
return res.redirect('/admin/ipacl');
}
}
}
res.send(await render(req, 'IPACL', content, {
}, , error, 'ipacl'));
});
if(minor >= 18) wiki.all(/^\/aclgroup\/create$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
if(!hasperm(req, 'aclgroup')) return res.send(await showError(req, 'permission'));
var content = `
<form method=post>
<label>그룹 이름: </label>
<input type=text name=group class=form-control />
<button type=submit class="btn btn-primary" style="width: 100px;">생성</button>
</form>
`;
var error = null;
if(req.method == 'POST') do {
const { group } = req.body;
if(!group) {
content = (error = err('alert', { code: 'validator_required', tag: 'group' })) + content;
break;
} else {
var data = await curs.execute("select name from aclgroup_groups where name = ?", [group]);
if(data.length) {
content = (error = err('alert', { code: 'aclgroup_already_exists' })) + content;
break;
}
else {
await curs.execute("insert into aclgroup_groups (name) values (?)", [group]);
return res.redirect('/aclgroup?group=' + encodeURIComponent(group));
}
}
} while(0);
res.send(await render(req, 'ACL그룹 생성', content, {}, , error, _));
});
if(minor >= 18) wiki.post(/^\/aclgroup\/delete$/, async(req, res, next) => {
if(!hasperm(req, 'aclgroup')) return res.send(await showError(req, 'permission'));
const { group } = req.body;
if(!group) return res.redirect('/aclgroup'); // 귀찮음
await curs.execute("delete from aclgroup_groups where name = ?", [group]);
res.redirect('/aclgroup');
});
if(minor >= 18) wiki.post(/^\/aclgroup\/remove$/, async(req, res) => {
if(!hasperm(req, 'aclgroup')) return res.send(await showError(req, 'permission'));
if(!req.body['id']) return res.status(400).send(await showError(req, { code: 'validator_required', tag: 'id' }));
var dbdata = await curs.execute("select username, aclgroup from aclgroup where id = ?", [req.body['id']]);
if(!dbdata.length) return res.status(400).send(await showError(req, 'invalid_value'));
await curs.execute("delete from aclgroup where id = ?", [req.body['id']]);
var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(data.length) logid = Number(data[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'aclgroup_remove',
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
id: req.body['id'],
target: dbdata[0].username,
note: req.body['note'] || ,
aclgroup: dbdata.aclgroup,
logid,
});
return res.redirect('/aclgroup?group=' + encodeURIComponent(dbdata.aclgroup));
});
if(minor >= 18) wiki.all(/^\/aclgroup$/, async(req, res) => {
if(!['POST', 'GET'].includes(req.method)) return next();
var data = await curs.execute("select name from aclgroup_groups", []);
const editable = hasperm(req, 'aclgroup');
var tabs = ``;
const group = req.query['group'] || (data.length ? data[0].name : null);
for(var g of data) {
const delbtn = `<form method=post onsubmit="return confirm('삭제하시겠습니까?');" action="/aclgroup/delete?group=${encodeURIComponent(g.name)}" style="display: inline-block; margin: 0; padding: 0;"><input type=hidden name=group value="${html.escape(g.name)}" /><button type=submit style="background: none; border: none; padding: 0; margin: 0;">×</button></form>`;
tabs += `
<a class="nav-link${g.name == group ? ' active' : }" href="?group=${encodeURIComponent(g.name)}">${html.escape(g.name)} ${editable ? delbtn : }</a>
`;
}
var content = `
<form method=post class="settings-section">
<select class=form-control name=mode>
<option value=ip>아이피</option>
<option value=username>사용자 이름</option>
</select>
<input type="text" class=form-control name="username" />
<label class=control-label>메모 :</label>
<input type="text" class=form-control id="noteInput" name="note" />
<label class=control-label>기간 :</label>
<select class=form-control name="expire">
${expireopt(req)}
</select>
<button type="submit" class="btn btn-primary" style="width: 90px;" ${!editable ? 'disabled' : }>추가</button>
</form>
`;
const navbtns = navbtn(0, 0, 0, 0);
if(group) {
content += `
${navbtns}
<form class="form-inline pull-right" id="searchForm" method=get>
<input type="text" class=form-control id="searchQuery" name="from" placeholder="ID" />
<button type=submit class="btn btn-primary">Go</button>
</form>
<colgroup>
<col style="width: 150px;">
<col style="width: 150px;">
<col>
<col style="width: 200px">
<col style="width: 160px">
<col style="width: 60px;">
</colgroup>
<thead>
</thead>
<tbody>
`;
var tr = ;
await curs.execute("delete from aclgroup where not expiration = '0' and ? > cast(expiration as integer)", [Number(getTime())]);
var data = await curs.execute("select id, type, username, expiration, note, date from aclgroup where aclgroup = ? order by cast(id as integer) desc limit 50", [group]);
for(var row of data) {
tr += `
`;
}
content += tr;
if(!tr) content += `
`;
content += `
</tbody>
ID
대상
메모
생성일
만료일
작업
${row.id}
${row.username}
${row.note}
${generateTime(toDate(row.date), timeFormat)}
${!Number(row.expiration) ? '영구' : generateTime(toDate(row.expiration), timeFormat)}
<form method=post onsubmit="return confirm('정말로?');" action="/aclgroup/remove">
<input type=hidden name=id value="${row.id}" />
<input type=hidden name=note value="" />
<input type=submit class="btn btn-sm btn-danger" value="삭제" />
</form>
ACL 그룹이 비어있습니다.
`;
}
var error = null;
if(req.method == 'POST') {
if(!hasperm(req, 'aclgroup')) return res.status(403).send(await showError(req, 'permission'));
var { mode, username, expire, note } = req.body;
if(!['ip', 'username'].includes(mode) || !username || !expire || note == undefined) error = true, content = alertBalloon(fetchErrorString('invalid_value'), 'danger', true, 'fade in') + content;
else {
if(mode == 'ip' && !username.includes('/')) username += '/32';
if(mode == 'ip' && !username.match(/^([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])[.]([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\/([1-9]|[12][0-9]|3[0-2])$/)) error = true, content = alertBalloon(fetchErrorString('invalid_cidr'), 'danger', true, 'fade in') + content;
else {
const date = getTime();
const expiration = expire == '0' ? '0' : String(Number(date) + Number(expire) * 1000);
var data = await curs.execute("select username from aclgroup where aclgroup = ? and type = ? and username = ? limit 1", [group, mode, username]);
if(data.length) error = true, content = alertBalloon(fetchErrorString('aclgroup_already_exists'), 'danger', true, 'fade in') + content;
var data = await curs.execute("select id from aclgroup order by cast(id as integer) desc limit 1");
var id = 1;
if(data.length) id = Number(data[0].id) + 1;
await curs.execute("insert into aclgroup (id, type, username, expiration, note, date, aclgroup) values (?, ?, ?, ?, ?, ?, ?)", [String(id), mode, username, expiration, note, date, group]);
var logid = 1, data = await curs.execute('select logid from block_history order by cast(logid as integer) desc limit 1');
if(data.length) logid = Number(data[0].logid) + 1;
insert('block_history', {
date: getTime(),
type: 'aclgroup_add',
aclgroup: group,
id: String(id),
duration: expire,
note,
ismember: islogin(req) ? 'author' : 'ip',
executer: ip_check(req),
target: username,
logid,
});
return res.redirect('/aclgroup?group=' + encodeURIComponent(group));
}
}
}
res.send(await render(req, 'ACLGroup', content, {
}, , error, 'aclgroup'));
});
wiki.all(/^\/Upload$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
const licelst = await curs.execute("select title from documents where namespace = '틀' and title like '이미지 라이선스/%' order by title");
const catelst = await curs.execute("select title from documents where namespace = '분류' and title like '파일/%' order by title");
var liceopts = , cateopts = ;
for(var lice of licelst) {
liceopts += `<option value="${html.escape( + totitle(lice.title, '틀'))}"${lice.title == '이미지 라이선스/제한적 이용' ? ' selected' : }>${html.escape(lice.title.replace('이미지 라이선스/', ))}</option>`;
}
for(var cate of catelst) {
cateopts += `<option value="${html.escape( + totitle(cate.title, '분류'))}">${html.escape(cate.title.replace('파일/', ))}</option>`;
}
var content = ;
content = `
<form method=post id="uploadForm" enctype="multipart/form-data" accept-charset="utf8">
<input type=hidden name=baserev value="0" />
<input type="file" id="fileInput" name="file" hidden="" />
<input type=hidden name=identifier value="${islogin(req) ? 'm' : 'i'}:${html.escape(ip_check(req))}" />
<label class=control-label for="fakeFileInput">파일 선택</label>
<input type="text" class=form-control id="fakeFileInput" readonly="" />
<button class="btn btn-secondary" type="button" id="fakeFileButton">Select</button>
<label class=control-label for="fakeFileInput">파일 이름</label>
<input type="text" class=form-control name="document" id=documentInput value="${html.escape(req.method == 'POST' ? req.body['document'] : )}" />
<textarea name="text" type="text" rows="25" id="textInput" class=form-control>${(req.method == 'POST' ? req.body['text'] : ).replace(/<\/(textarea)>/gi, '</$1>')}</textarea>
${req.method == 'GET' ? `
<label class=control-label for="licenseSelect">라이선스</label>
<select id=licenseSelect class=form-control>${ liceopts }</select>
[주의!] 파일문서의 라이선스(문서 본문)와 올리는 파일의 라이선스는 다릅니다. 파일의 라이선스를 올바르게 지정하였는지 확인하세요.
<label class=control-label for="categorySelect">분류</label>
<select id=categorySelect class=form-control>
<option value>선택</option>
${cateopts}
</select>
` : }
<label class=control-label>요약</label>
<input type="text" id="logInput" class=form-control name="log" value="${html.escape(req.method == 'POST' ? req.body['log'] : )}" />
${config.getString('wiki.editagree_text', `문서 편집을 저장하면 당신은 기여한 내용을 CC-BY-NC-SA 2.0 KR으로 배포하고 기여한 문서에 대한 하이퍼링크나 URL을 이용하여 저작자 표시를 하는 것으로 충분하다는 데 동의하는 것입니다. 이 동의는 철회할 수 없습니다.`)}
${islogin(req) ? : `
비로그인 상태로 편집합니다. 편집 역사에 IP(${ip_check(req)})가 영구히 기록됩니다.
`}
<button id="uploadBtn" type="submit" class="btn btn-primary">올리기</button>
</form>
<script>uploadInit();</script>
`;
var error = null;
if(req.method == 'POST') do {
var file = req.files[0];
if(!file) { content = (error = err('alert', { code: 'file_not_uploaded' })) + content; break; }
var title = req.body['document'];
if(!title) { content = (error = err('alert', { code: 'validator_required', tag: 'document' })) + content; break; }
var doc = processTitle(title);
if(doc.namespace != '파일') { content = (error = err('alert', { msg: '업로드는 파일 이름 공간에서만 가능합니다.' })) + content; break; }
if(path.extname(doc.title).toLowerCase() != path.extname(file.originalname).toLowerCase()) {
content = (error = err('alert', { msg: '문서 이름과 확장자가 맞지 않습니다.' })) + content;
break;
}
var aclmsg = await getacl(req, doc.title, doc.namespace, 'edit', 1);
if(aclmsg) { content = (error = err('alert', { code: 'permission_edit', msg: aclmsg })) + content; break; }
if(error) break;
var request = http.request({
method: 'POST',
host: hostconfig.image_host,
port: hostconfig.image_port,
path: '/upload',
headers: {
'Content-Type': 'application/json',
},
}, async res => {
var data = ;
res.on('data', chunk += data);
res.on('end', async () => {
data = JSON.parse(data);
if(data.status != 'success') {
error = err('alert', { code: 'file_not_uploaded' });
return res.send(await render(req, '파일 올리기', error + content, {}, _, error, 'upload'));
}
await curs.execute("insert into files (title, namespace, hash) values (?, ?, ?)", [doc.title, doc.namespace, ]); // sha224 해시화 필요
return res.redirect('/w/' + totitle(doc.title, doc.namespace));
});
}).on('error', async e => {
error = err('alert', { msg: '파일 서버가 사용가능하지 않습니다.' });
return res.send(await render(req, '파일 올리기', error + content, {}, _, error, 'upload'));
});
request.write(JSON.stringify({
filename: file.originalname,
document: title,
mimetype: file.mimetype,
file: file.buffer.toString('base64'),
}));
request.end();
return;
} while(0);
res.send(await render(req, '파일 올리기', content, {}, _, error, 'upload'));
});
wiki.get(/^\/BlockHistory$/, async(req, res) => {
var pa = [];
var qq = " where '1' = '1' ";
if(req.query['target'] && req.query['query']) {
const com = req.query['query'].startsWith('"') && req.query['query'].endsWith('"');
const query = com ? req.query['query'].replace(/^\"/, ).replace(/\"$/, ) : req.query['query'];
if(req.query['target'] == 'author') {
qq = 'where executer' + (com ? ' = ? ' : "like '%' || ? || '%' ");
pa = [query];
} else {
qq = 'where note ' + (com ? ' = ? ' : "like '%' || ? || '%' ") + ' or target ' + (com ? ' = ? ' : "like '%' || ? || '%' ");
pa = [query, query];
}
}
var total = (await curs.execute("select count(logid) from block_history"))[0]['count(logid)'];
const from = req.query['from'];
const until = req.query['until'];
var data;
if(from) {
data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " +
qq + " and (cast(logid as integer) <= ? AND cast(logid as integer) > ?) order by cast(date as integer) desc limit 100",
pa.concat([Number(from), Number(from) - 100]));
} else if(until) {
data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " +
qq + " and (cast(logid as integer) >= ? AND cast(logid as integer) < ?) order by cast(date as integer) desc limit 100",
pa.concat([Number(until), Number(until) + 100]));
} else {
data = await curs.execute("select logid, date, type, aclgroup, id, duration, note, executer, target, ismember from block_history " +
qq + " order by cast(date as integer) desc limit 100",
pa);
}
try {
var navbtns = navbtn(total, data[data.length-1].logid, data[0].logid, '/BlockHistory');
} catch(e) {
var navbtns = navbtn(0, 0, 0, 0);
}
var content = `
<form>
<select name="target">
<option value="text"${req.query['target'] == 'text' ? ' selected' : }>내용</option>
<option value="author"${req.query['target'] == 'author' ? ' selected' : }>실행자</option>
</select>
<input name="query" placeholder="검색" type="text" value="${html.escape(req.query['query']) || }" />
<input value="검색" type="submit" />
</form>
${navbtns}
`;
function parses(s) {
s = Number(s);
var ret = ;
if(s && s / 604800 >= 1) (ret += parseInt(s / 604800) + '주 '), s = s % 604800;
if(s && s / 86400 >= 1) (ret += parseInt(s / 86400) + '일 '), s = s % 86400;
if(s && s / 3600 >= 1) (ret += parseInt(s / 3600) + '시간 '), s = s % 3600;
if(s && s / 60 >= 1) (ret += parseInt(s / 60) + '분 '), s = s % 60;
if(s && s / 1 >= 1) (ret += parseInt(s / 1) + '초 '), s = s % 1;
return ret.replace(/\s$/, );
}
for(var item of data) {
if(['aclgroup_add', 'aclgroup_remove'].includes(item.type) && minor < 18) continue;
content += `
- ${generateTime(toDate(item.date), timeFormat)} ${ip_pas(item.executer, item.ismember)} 사용자가 ${item.target} (${
item.type == 'aclgroup_add'
? `${item.aclgroup} ACL 그룹에 추가`
: (
item.type == 'aclgroup_remove'
? `${item.aclgroup} ACL 그룹에서 제거`
: (
item.type == 'ipacl_add'
? `IP 주소 차단`
: (
item.type == 'ipacl_remove'
? `IP 주소 차단 해제`
: (
item.type == 'login_history'
? `사용자 로그인 기록 조회`
: (
item.type == 'suspend_account' && item.duration != '-1'
? `사용자 차단`
: (
item.type == 'suspend_account' && item.duration == '-1'
? `사용자 차단 해제`
: (
item.type == 'grant'
? `사용자 권한 설정`
:
)))))))
}) ${item.type == 'aclgroup_add' || item.type == 'aclgroup_remove' ? `#${item.id}` : } ${
item.type == 'aclgroup_add' || item.type == 'ipacl_add' || (item.type == 'suspend_account' && item.duration != '-1')
? (major == 4 && (minor >= 1 || (minor == 0 && revision >= 20)) ? `(${item.duration == '0' ? '영구적으로' : `${parses(item.duration)} 동안`})` : `${item.duration} 동안`)
:
} ${
item.type == 'aclgroup_add' || item.type == 'ipacl_add' || item.type == 'suspend_account' || item.type == 'grant'
? `(${item.note})`
:
}
`;
}
content += `
${navbtns}
`;
return res.send(await render(req, '차단 내역', content, {}, _, _, 'block_history'));
});
wiki.get(/^\/settings$/, async(req, res) => {
res.send(await render(req, '스킨 설정', '이 스킨은 설정 기능을 지원하지 않습니다.', {}, _, _, 'settings'));
});
if(hostconfig.allow_account_deletion) wiki.all(/^\/member\/delete_account$/, async(req, res, next) => {
if(!['GET', 'POST'].includes(req.method)) return next();
if(!islogin(req)) return res.redirect('/member/login?redirect=%2Fmember%2Fdelete_account');
const username = ip_check(req);
var error = false;
var { password } = (await curs.execute("select password from users where username = ?", [username]))[0];
var content = `
<form method=post onsubmit="return confirm('마지막 경고입니다. 탈퇴하려면 [확인]을 누르십시오.');">
계정을 삭제하면 문서 역사에서 당신의 사용자 이름이 익명화됩니다. 문서 배포 라이선스가 퍼블릭 도메인이 아닌 경우 가급적 탈퇴는 자제해주세요.
<label>사용자 이름을 확인해주세요 (${html.escape(username)}):</label>
<input type=text name=username class=form-control placeholder="${html.escape(username)}" value="${html.escape(req.body['username'] || )}" />
${!error && req.method == 'POST' && req.body['username'] != username ? (error = true, `자신의 사용자 이름을 입력해주세요.
`) : }
<label>비밀번호 확인:</label>
<input type=password name=password class=form-control />
${!error && req.method == 'POST' && sha3(req.body['password'] + ) != password ? (error = true, `비밀번호를 확인해주세요.
`) : }
<a class="btn btn-secondary" href="/">취소</a>
<a class="btn btn-secondary" href="/">취소</a>
<a class="btn btn-secondary" href="/">취소</a>
<a class="btn btn-secondary" href="/">취소</a>
<button type=submit class="btn btn-danger">삭제</button>
<a class="btn btn-secondary" href="/">취소</a>
<a class="btn btn-secondary" href="/">취소</a>
</form>
`;
if(req.method == 'POST' && !error) {
curs.execute("delete from users where username = ?", [username]);
curs.execute("delete from perms where username = ?", [username]);
curs.execute("delete from suspend_account where username = ?", [username]);
curs.execute("delete from user_settings where username = ?", [username]);
curs.execute("delete from acl where title = ? and namespace = '사용자'", [username]);
curs.execute("delete from classic_acl where title = ? and namespace = '사용자'", [username]);
curs.execute("delete from documents where title = ? and namespace = '사용자'", [username]);
curs.execute("delete from history where title = ? and namespace = '사용자'", [username]);
curs.execute("delete from login_history where username = ?", [username]);
curs.execute("delete from stars where username = ?", [username]);
curs.execute("delete from useragents where username = ?", [username]);
curs.execute("update history set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]);
curs.execute("update res set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]);
curs.execute("update res set hider = '탈퇴한 사용자' where hider = ?", [username]);
curs.execute("update block_history set executer = '탈퇴한 사용자', ismember = 'ip' where executer = ? and ismember = 'author'", [username]);
curs.execute("update block_history set target = '탈퇴한 사용자' where target = ?", [username]);
curs.execute("update edit_requests set processor = '탈퇴한 사용자', ismember = 'ip' where processor = ? and ismember = 'author'", [username]);
curs.execute("update edit_requests set username = '탈퇴한 사용자', ismember = 'ip' where username = ? and ismember = 'author'", [username]);
delete req.session.username;
delete userset[username];
if(permlist[username]) permlist[username] = [];
res.cookie('honoka', , { expires: new Date(Date.now() - 1) });
return res.send(await render(req, '계정 삭제', `
${html.escape(username)}님 안녕히 가십시오.
`, {}, _, false, 'delete_account'));
}
return res.send(await render(req, '계정 삭제', content, {}, _, error, 'delete_account'));
});
if(hostconfig.allow_account_rename) wiki.all(/^\/member\/change_username$/, async(req, res, next) => {
if(!['GET', 'POST'].includes(req.method)) return next();
if(!islogin(req)) return res.redirect('/member/login?redirect=%2Fmember%2Fdelete_account');
const username = ip_check(req);
var error = false;
var { password } = (await curs.execute("select password from users where username = ?", [username]))[0];
if(req.method == 'POST') {
if(!req.body['new_username'])
var nonewusername = 1;
var data = await curs.execute("select username from users where lower(username) = ? COLLATE NOCASE", [req.body['new_username'].toLowerCase()]);
if(data.length)
var duplicate = 1;
if(!hostconfig.no_username_format && (id.length < 3 || id.length > 32 || id.match(/(?:[^A-Za-z0-9_])/)))
var invalidformat = 1;
}
var content = `
<form method=post onsubmit="return confirm('마지막 경고입니다. 변경하려면 [확인]을 누르십시오.');">
${!error && req.method == 'POST' && nonewusername ? (error = true, alertBalloon(fetchErrorString('validator_required', 'new_username'), 'danger', true, 'fade in')) : }
${(hostconfig.owners || []).includes(username) ? `
수정 후 반드시 config.json의 <owners> 값을 바꿔 주세요.
` : }
이름을 바꾸면 다른 사람이 당신의 기존 이름으로 가입할 수 있습니다.
<label>현재 이름 확인 (${html.escape(username)}):</label>
<input type=text name=username class=form-control placeholder="${html.escape(username)}" value="${html.escape(req.body['username'] || )}" />
${!error && req.method == 'POST' && req.body['username'] != username ? (error = true, `자신의 사용자 이름을 입력해주세요.
`) : }
<label>비밀번호 확인:</label>
<input type=password name=password class=form-control />
${!error && req.method == 'POST' && sha3(req.body['password'] + ) != password ? (error = true, `비밀번호를 확인해주세요.
`) : }
<label>새로운 사용자 이름:</label>
<input type=text name=new_username class=form-control value="${html.escape(req.body['new_username'] || )}" />
${!error && req.method == 'POST' && duplicate ? (error = true, `사용자 이름이 이미 존재합니다.
`) : }
${!error && req.method == 'POST' && invalidformat ? (error = true, `사용자 이름을 형식에 맞게 입력해주세요.
`) : }
<a class="btn btn-secondary" href="/">취소</a>
<a class="btn btn-secondary" href="/">취소</a>
<button type=submit class="btn btn-danger">변경</button>
<a class="btn btn-secondary" href="/">취소</a>
<a class="btn btn-secondary" href="/">취소</a>
<a class="btn btn-secondary" href="/">취소</a>
</form>
`;
if(req.method == 'POST' && !error) {
var newusername = req.body['new_username'];
await curs.execute("update users set username = ? where username = ?", [newusername, username]);
await curs.execute("update perms set username = ? where username = ?", [newusername, username]);
await curs.execute("update suspend_account set username = ? where username = ?", [newusername, username]);
await curs.execute("update user_settings set username = ? where username = ?", [newusername, username]);
await curs.execute("update acl set title = ? where title = ? and namespace = '사용자'", [newusername, username]);
await curs.execute("update classic_acl set title = ? where title = ? and namespace = '사용자'", [newusername, username]);
await curs.execute("update documents set title = ? where title = ? and namespace = '사용자'", [newusername, username]);
await curs.execute("update threads set title = ? where title = ? and namespace = '사용자'", [newusername, username]);
await curs.execute("update edit_requests set title = ? where title = ? and namespace = '사용자'", [newusername, username]);
await curs.execute("update history set title = ? where title = ? and namespace = '사용자'", [newusername, username]);
await curs.execute("update login_history set username = ? where username = ?", [newusername, username]);
await curs.execute("update stars set username = ? where username = ?", [newusername, username]);
await curs.execute("update useragents set username = ? where username = ?", [newusername, username]);
await curs.execute("update history set username = ? where username = ? and ismember = 'author'", [newusername, username]);
await curs.execute("update res set username = ? where username = ? and ismember = 'author'", [newusername, username]);
await curs.execute("update res set hider = ? where hider = ?", [newusername, username]);
await curs.execute("update block_history set executer = ? where executer = ? and ismember = 'author'", [newusername, username]);
await curs.execute("update block_history set target = ? where target = ?", [newusername, username]);
await curs.execute("update edit_requests set processor = ? where processor = ? and ismember = 'author'", [newusername, username]);
await curs.execute("update edit_requests set username = ? where username = ? and ismember = 'author'", [newusername, username]);
await curs.execute("update autologin_tokens set username = ? where username = ?", [newusername, username]);
req.session.username = newusername;
permlist[newusername] = permlist[username];
delete permlist[username];
userset[newusername] = userset[username];
delete userset[username];
return res.send(await render(req, '사용자 이름 변경', `
${html.escape(newusername)}로 이름을 변경하였습니다.
`, {}, _, false, 'delete_account'));
}
return res.send(await render(req, '사용자 이름 변경', content, {}, _, error, 'delete_account'));
});
wiki.all(/^\/member\/mypage$/, async(req, res, next) => {
if(!['GET', 'POST'].includes(req.method)) return next();
if(!islogin(req)) return res.redirect('/member/login?redirect=%2Fmember%2Fmypage');
var myskin = getUserset(req, 'skin', 'default');
const defskin = config.getString('wiki.default_skin', hostconfig.skin);
var skopt = ;
for(var skin of skinList) {
var opt = `<option value="${skin}" ${getUserset(req, 'skin', 'default') == skin ? 'selected' : }>${skin}</option>`;
skopt += opt;
}
var error = null;
var emailfilter = ;
if(config.getString('wiki.email_filter_enabled', 'false') == 'true') {
emailfilter = `
이메일 허용 목록이 활성화 되어 있습니다.
이메일 허용 목록에 존재하는 메일만 사용할 수 있습니다.
`;
var filters = await curs.execute("select address from email_filters");
for(var item of filters) {
emailfilter += '- ' + item.address + '
';
}
emailfilter += '
';
}
var content = `
<form method=post>
<label>사용자 이름</label>
<input type=text name=username readonly class=form-control value="${html.escape(ip_check(req))}" />
<label>전자우편 주소</label>
<input type=email name=email class=form-control value="${html.escape(getUserset(req, 'email', ))}" />
${emailfilter}
<label>암호</label>
<input type=password name=password class=form-control />
<label>암호 확인</label>
<input type=password name=password_check class=form-control />
${req.method == 'POST' && req.body['password'] && req.body['password'] != req.body['password_check'] ? (error = true, `패스워드 확인이 올바르지 않습니다.
`) : }
<label>스킨</label>
<select name=skin class=form-control>
<option value=default ${myskin == 'default' ? 'selected' : }>기본스킨 (${defskin})</option>
${skopt}
</select>
${req.method == 'POST' && !skinList.concat(['default']).includes(req.body['skin']) ? (error = err('p', 'invalid_skin')) : }
<label>Google Authenticator<label>
<a class="btn btn-info" href="/member/activate_otp">활성화</a>
<button type=reset class="btn btn-secondary">초기화</button>
<button type=submit class="btn btn-primary">변경</button>
</form>
`;
if(req.method == 'POST' && !error) {
for(var item of ['skin']) {
await curs.execute("delete from user_settings where username = ? and key = ?", [ip_check(req), item]);
await curs.execute("insert into user_settings (username, key, value) values (?, ?, ?)", [ip_check(req), item, req.body[item] || ]);
userset[ip_check(req)][item] = req.body[item] || ;
}
if(req.body['password']) {
await curs.execute("update users set password = ? where username = ?", [sha3(req.body['password']), ip_check(req)]);
}
return res.redirect('/member/mypage');
}
return res.send(await render(req, '내 정보', content, {}, _, error, 'mypage'));
});
wiki.get(/^\/member\/logout$/, async(req, res, next) => {
var autologin;
if(autologin = req.cookies['honoka']) {
await curs.execute("delete from autologin_tokens where token = ?", [autologin]);
res.cookie('honoka', , { expires: new Date(Date.now() - 1) });
}
var desturl = req.query['redirect'];
if(!desturl) desturl = '/';
delete req.session.username;
res.redirect(desturl);
});
wiki.all(/^\/member\/login$/, async function loginScreen(req, res, next) {
if(!['GET', 'POST'].includes(req.method)) return next();
var desturl = req.query['redirect'];
if(!desturl) desturl = '/';
if(islogin(req)) return res.redirect(desturl);
var id = '1', pw = '1';
var error = null;
if(req.method == 'POST') do {
id = req.body['username'] || ;
pw = req.body['password'] || ;
if(!id) break;
var data = await curs.execute("select username from users where lower(username) = ? COLLATE NOCASE", [id.toLowerCase()]);
var invalidusername = !id || !data.length;
if(invalidusername) break;
var usr = data;
if(!pw) break;
var data = await curs.execute("select username, password from users where lower(username) = ? and password = ? COLLATE NOCASE", [id.toLowerCase(), sha3(pw)]);
var invalidpw = !invalidusername && (!data.length || !pw);
if(invalidpw) break;
var blocked = (major > 4 || (major == 4 && minor >= 1)) ? 0 : await userblocked(id);
if(blocked) break;
} while(0);
var content = `
<form class=login-form method=post>
<label>Username</label>
<input class=form-control name="username" type="text" value="${html.escape(req.method == 'POST' ? req.body['username'] : )}" />
${req.method == 'POST' && !error && !id.length ? (error = err('p', { code: 'validator_required', tag: 'username' })) : }
${req.method == 'POST' && !error && invalidusername ? (error = err('p', 'invalid_username')) : }
${req.method == 'POST' && !error && blocked ? (error = err('p', { msg: `차단된 계정입니다.
차단 만료일 : ${(blocked.expiration == '0' ? '무기한' : new Date(Number(blocked.expiration)))}
차단 사유 : ${blocked.note}` })) : ``}
<label>Password</label>
<input class=form-control name="password" type="password" />
${req.method == 'POST' && !error && !pw.length ? (error = err('p', { code: 'validator_required', tag: 'password' })) : }
${req.method == 'POST' && !error && invalidpw ? (error = err('p', { msg: '암호가 올바르지 않습니다.' })) : }
<label>
<input type=checkbox name=autologin>
자동 로그인
</label>
<a href="/member/recover_password" style="float: right;">[아이디/비밀번호 찾기]</a>
<a href="/member/signup" class="btn btn-secondary">계정 만들기</a><button type="submit" class="btn btn-primary">로그인</button>
</form>
`;
if(req.method == 'POST' && !error) {
id = usr[0].username;
if(req.body['autologin']) {
const key = rndval('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/', 128);
res.cookie('honoka', key, {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 360),
httpOnly: true,
});
await curs.execute("insert into autologin_tokens (username, token) values (?, ?)", [id, key]);
}
if(!hostconfig.disable_login_history) {
curs.execute("insert into login_history (username, ip, time) values (?, ?, ?)", [id, ip_check(req, 1), getTime()]);
conn.run("delete from useragents where username = ?", [id], () => {
curs.execute("insert into useragents (username, string) values (?, ?)", [id, req.headers['user-agent']]);
});
}
req.session.username = id;
return res.redirect(desturl);
}
res.send(await render(req, '로그인', content, {}, _, error, 'login'));
});
wiki.all(/^\/member\/signup$/, async function signupEmailScreen(req, res, next) {
if(!['GET', 'POST'].includes(req.method)) return next();
var desturl = req.query['redirect'];
if(!desturl) desturl = '/';
if(islogin(req)) { res.redirect(desturl); return; }
var emailfilter = ;
if(config.getString('wiki.email_filter_enabled', 'false') == 'true') {
emailfilter = `
이메일 허용 목록이 활성화 되어 있습니다.
이메일 허용 목록에 존재하는 메일만 사용할 수 있습니다.
`;
for(var item of await curs.execute("select address from email_filters")) {
emailfilter += '- ' + item.address + '
';
}
emailfilter += '
';
}
var bal = ;
var error = null;
if(hostconfig.disable_email) req.body['email'] = ;
if(req.method == 'POST') do {
var blockmsg = await ipblocked(ip_check(req, 1));
if(blockmsg) break;
if(!hostconfig.disable_email && (!req.body['email'] || req.body['email'].match(/[@]/g).length != 1)) {
var invalidemail = 1;
break;
}
var data = await curs.execute("select email from account_creation where email = ?", [req.body['email']]);
if(!hostconfig.disable_email && data.length) {
var duplicate = 1;
break;
}
var data = await curs.execute("select value from user_settings where key = 'email' and value = ?", [req.body['email']]);
if(!hostconfig.disable_email && data.length) {
var userduplicate = 1;
break;
}
if(emailfilter) {
var data = await curs.execute("select address from email_filters where address = ?", [req.body['email'].split('@')[1]]);
if(!hostconfig.disable_email && !data.length) {
var filteredemail = 1;
break;
}
}
} while(0);
var content = `
${req.method == 'POST' && !error && filteredemail ? (error = err('alert', { msg: '이메일 허용 목록에 있는 이메일이 아닙니다.' })) : }
<form method=post class=signup-form>
<label>전자우편 주소</label>
${hostconfig.disable_email ? `
<input type=hidden name=email value="" />
비활성화됨
` : `<input type=email name=email class=form-control />`}
${req.method == 'POST' && !error && duplicate ? (error = err('p', { msg: '해당 이메일로 이미 계정 생성 인증 메일을 보냈습니다.' })) : }
${req.method == 'POST' && !error && userduplicate ? (error = err('p', { msg: '이메일이 이미 존재합니다.' })) : }
${req.method == 'POST' && !error && invalidemail ? (error = err('p', { msg: '이메일의 값을 형식에 맞게 입력해주세요.' })) : }
${emailfilter}
가입후 탈퇴는 불가능합니다.
<button type=reset class="btn btn-secondary">초기화</button>
<button type=submit class="btn btn-primary">가입</button>
</form>
`;
if(req.method == 'POST' && !error) {
await curs.execute("delete from account_creation where cast(time as integer) < ?", [Number(getTime()) - 86400000]);
const key = rndval('abcdef1234567890', 64);
curs.execute("insert into account_creation (key, email, time) values (?, ?, ?)", [key, req.body['email'], String(getTime())]);
if(hostconfig.disable_email) return res.redirect('/member/signup/' + key);
return res.send(await render(req, '계정 만들기', `
이메일(${req.body['email']})로 계정 생성 이메일 인증 메일을 전송했습니다. 메일함에 도착한 메일을 통해 계정 생성을 계속 진행해 주시기 바랍니다.
- 간혹 메일이 도착하지 않는 경우가 있습니다. 이 경우, 스팸함을 확인해주시기 바랍니다.
- 인증 메일은 24시간동안 유효합니다.
${hostconfig.debug ?
`
[디버그] 가입 주소: <a href="/member/signup/${key}">/member/signup/${key}</a>
` : }
`, {}));
}
res.send(await render(req, '계정 만들기', content, {}, _, error, 'signup'));
});
wiki.all(/^\/member\/signup\/(.*)$/, async function signupScreen(req, res, next) {
if(!['GET', 'POST'].includes(req.method)) return next();
await curs.execute("delete from account_creation where cast(time as integer) < ?", [Number(getTime()) - 86400000]);
const key = req.params[0];
var credata = await curs.execute("select email from account_creation where key = ?", [key]);
if(!credata.length) {
return res.send(await showError(req, 'invalid_signup_key'));
}
var desturl = req.query['redirect'];
if(!desturl) desturl = '/';
if(islogin(req)) { res.redirect(desturl); return; }
var id = '1', pw = '1', pw2 = '1';
var content = ;
var error = null;
if(req.method == 'POST') do {
id = req.body['username'] || ;
pw = req.body['password'] || ;
pw2 = req.body['password_check'] || ;
if(!hostconfig.no_username_format && (id.length < 3 || id.length > 32 || id.match(/(?:[^A-Za-z0-9_])/))) {
var invalidformat = 1;
break;
}
if((hostconfig.reserved_usernames || []).concat(['namubot']).includes(id)) {
var invalidusername = 1;
break;
}
var data = await curs.execute("select username from users where lower(username) = ? COLLATE NOCASE", [id.toLowerCase()]);
if(data.length) {
var duplicate = 1;
break;
}
} while(0);
content += `
<form class=signup-form method=post>
<label>사용자 ID</label>
<input class=form-control name="username" type="text" value="${html.escape(req.method == 'POST' ? req.body['username'] : )}" />
${req.method == 'POST' && !error && !id.length ? (error = err('p', { code: 'validator_required', tag: 'username' })) : }
${req.method == 'POST' && !error && duplicate ? (error = err('p', 'username_already_exists')) : }
${req.method == 'POST' && !error && invalidusername ? (error = err('p', 'invalid_username')) : }
${req.method == 'POST' && !error && invalidformat ? (error = err('p', 'username_format')) : }
<label>암호</label>
<input class=form-control name="password" type="password" />
${req.method == 'POST' && !error && !pw.length ? (error = err('p', { code: 'validator_required', tag: 'password' })) : }
<label>암호 확인</label>
<input class=form-control name="password_check" type="password" />
${req.method == 'POST' && !error && !pw2.length ? (error = err('p', { code: 'validator_required', tag: 'password_check' })) : }
${req.method == 'POST' && !error && pw2 != pw ? (error = err('p', { msg: '암호 확인이 올바르지 않습니다.' })) : }
가입후 탈퇴는 불가능합니다.
<button type=reset class="btn btn-secondary">초기화</button>
<button type=submit class="btn btn-primary">가입</button>
</form>
`;
if(req.method == 'POST' && !error) do {
var baserev = 0;
var data = await curs.execute("select rev from history where title = ? and namespace = ? order by CAST(rev AS INTEGER) desc limit 1", [id, '사용자']);
if(data.length) baserev = Number(data[0].rev);
var data = await curs.execute("select title from documents where title = ? and namespace = ?", [id, '사용자']);
if(data.length) {
error = err('alert', 'edit_conflict');
content = error + content;
break; }
permlist[id] = [];
var data = await curs.execute("select username from users");
if(!data.length) {
for(var perm of perms) {
if(disable_autoperms.includes(perm)) continue;
curs.execute(`insert into perms (username, perm) values (?, ?)`, [id, perm]);
permlist[id].push(perm);
}
}
req.session.username = id;
await curs.execute("insert into users (username, password) values (?, ?)", [id, sha3(pw)]);
await curs.execute("insert into user_settings (username, key, value) values (?, 'email', ?)", [id, credata[0].email]);
await curs.execute("insert into documents (title, namespace, content) values (?, '사용자', )", [id]);
await curs.execute("insert into history (title, namespace, content, rev, time, username, changes, log, iserq, erqnum, advance, ismember) \
values (?, '사용자', , ?, ?, ?, '0', , '0', , 'create', 'author')", [
id, String(baserev + 1), getTime(), id
]);
if(!hostconfig.disable_login_history) {
await curs.execute("insert into login_history (username, ip) values (?, ?)", [id, ip_check(req, 1)]);
await curs.execute("insert into useragents (username, string) values (?, ?)", [id, req.headers['user-agent']]);
}
await curs.execute("delete from account_creation where key = ?", [key]);
return res.send(await render(req, '계정 만들기', `
환영합니다! ${html.escape(id)}님 계정 생성이 완료되었습니다.
`, {}));
} while(0);
res.send(await render(req, '계정 만들기', content, {}, _, error, 'signup'));
});
wiki.get(/^\/random$/, async(req, res) => {
var data = await curs.execute("select title from documents where namespace = '문서' order by random() limit 1");
if(!data.length) res.redirect('/');
res.redirect('/w/' + encodeURIComponent(data[0].title));
});
wiki.get(/^\/RandomPage$/, async function randomPage(req, res) {
const nslist = fetchNamespaces();
var ns = req.query['namespace'];
if(!ns || !nslist.includes(ns)) ns = '문서';
var content = `
<fieldset class="recent-option">
<form class="form-inline" method=get>
<label class=control-label>이름공간 :</label>
<select class=form-control id=namespace name=namespace>
`;
for(var nsp of nslist) {
content += `
<option value="${nsp}"${nsp == ns ? ' selected' : }>${nsp == 'wiki' ? config.getString('wiki.site_name', '더 시드') : nsp}</option>
`;
}
content += `
</select>
<button type=submit class="btn btn-primary" style="width: 5rem;">제출</button>
</form>
</fieldset>
`;
var cnt = 0, li = ;
while(cnt < 20) {
let data = await curs.execute("select title from documents where namespace = ? order by random() limit 20", [ns]);
if(!data.length) break;
for(let i of data) {
li += '- <a href="/w/' + encodeURIComponent(totitle(i.title, ns)) + '">' + html.escape(totitle(i.title, ns) + ) + '</a>
';
cnt++;
if(cnt > 19) break;
}
if(cnt > 19) break;
}
content += (li || '- <a href="/w/' + encodeURIComponent(config.getString('wiki.front_page', 'FrontPage')) + '">' + html.escape(config.getString('wiki.front_page', 'FrontPage')) + '</a>
') + '
';
res.send(await render(req, 'RandomPage', content, {}));
});
wiki.get(/^\/NeededPages$/, async(req, res) => {
const nslist = fetchNamespaces();
var ns = req.query['namespace'];
if(!ns || !nslist.includes(ns)) ns = '문서';
if(!neededPages[ns]) neededPages[ns] = [];
var ss;
var st, ed;
var total = neededPages[ns].length;
if(!req.query['from'] && req.query['until']) {
ss = Number(req.query['until']);
st = ss - 100, ed = ss;
if(ed > total) ed = total;
if(st < 1) st = 1;
} else {
ss = Number(req.query['from'] || '1');
st = ss, ed = ss + 100;
if(ed > total) ed = total;
if(st < 1) st = 1;
}
const navbtns = navbtnr(total, st, ed, '/NeededPages');
const ret = neededPages[ns].slice(st - 1, ed);
var content = `
<fieldset class=recent-option>
<form class=form-inline method=get>
<label class=control-label>이름공간 :</label>
<select class=form-control id=namespace name=namespace>
`;
for(var nsp of nslist) {
content += `
<option value="${nsp}"${nsp == ns ? ' selected' : }>${nsp == 'wiki' ? config.getString('wiki.site_name', '더 시드') : nsp}</option>
`;
}
content += `
</select>
<button type=submit class="btn btn-primary" style="width: 5rem;">제출</button>
</form>
</fieldset>
역 링크는 존재하나 아직 작성이 되지 않은 문서 목록입니다.
이 페이지는 하루에 한번 업데이트 됩니다.
${navbtns}
`;
for(let item of ret) {
content += '- <a href="/w/' + encodeURIComponent(totitle(item, ns)) + '">' + html.escape(totitle(item, ns) + ) + '</a> <a href="/' + (minor >= 14 ? 'backlink' : 'xref') + '/' + encodeURIComponent(totitle(item, ns)) + '">[역링크]</a>
';
}
content += '
' + navbtns;
res.send(await render(req, '작성이 필요한 문서', content, {}));
});
wiki.get(/^\/UncategorizedPages$/, async(req, res) => {
const nslist = fetchNamespaces();
var ns = req.query['namespace'];
if(!ns || !nslist.includes(ns)) ns = '문서';
var content = `
<fieldset class="recent-option">
<form class="form-inline" method=get>
<label class=control-label>이름공간 :</label>
<select class=form-control id=namespace name=namespace>
`;
for(var nsp of nslist) {
content += `
<option value="${nsp}"${nsp == ns ? ' selected' : }>${nsp == 'wiki' ? config.getString('wiki.site_name', '더 시드') : nsp}</option>
`;
}
content += `
</select>
<button type=submit class="btn btn-primary" style="width: 5rem;">제출</button>
</form>
</fieldset>
`;
let data = await curs.execute("select title, content from documents where namespace = ? order by title asc limit 100", [ns]);
for(let i of data) {
if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue;
const d = await curs.execute("select title from backlink where title = ? and namespace = ? and type = 'category'", [i.title, ns]);
if(d.length) continue;
content += '- <a href="/w/' + encodeURIComponent(totitle(i.title, ns)) + '">' + html.escape(totitle(i.title, ns) + ) + '</a>
';
}
content += '
';
res.send(await render(req, '분류가 되지 않은 문서', content, {}));
});
wiki.get(/^\/OldPages$/, async(req, res) => {
const nslist = fetchNamespaces();
var ns = req.query['namespace'];
if(!ns || !nslist.includes(ns)) ns = '문서';
var content = `
편집된 지 오래된 문서의 목록입니다. (리다이렉트 제외)
`;
let data = await curs.execute("select title, time, content from documents where namespace = '문서' order by cast(time as integer) asc limit 100");
for(let i of data) {
if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue;
content += '- <a href="/w/' + encodeURIComponent(totitle(i.title, '문서')) + '">' + html.escape(totitle(i.title, ns) + ) + `</a> (수정 시각:${generateTime(toDate(i.time), timeFormat)})`;
}
content += '
';
res.send(await render(req, '편집된 지 오래된 문서', content, {}));
});
wiki.get(/^\/ShortestPages$/, async function shortestPages(req, res) {
var from = req.query['from'];
if(!from) ns = '1';
var sql_num = 0;
if(from > 0)
sql_num = from - 122;
else
sql_num = 0;
var data = await curs.execute("select title, content from documents where namespace = '문서' order by length(content) limit ?, '122'", [sql_num]);
var content = `
내용이 짧은 문서 (문서 이름공간, 리다이렉트 제외)
${navbtn(0, 0, 0, 0)}
`;
for(var i of data) {
if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue;
content += '- <a href="/w/' + encodeURIComponent(i['title']) + '">' + html.escape(i['title']) + `</a> (${i.content.length}글자)
`;
}
content += '
' + navbtn(0, 0, 0, 0);
res.send(await render(req, '내용이 짧은 문서', content, {}));
});
wiki.get(/^\/LongestPages$/, async function longestPages(req, res) {
var from = req.query['from'];
if(!from) ns = '1';
var sql_num = 0;
if(from > 0)
sql_num = from - 122;
else
sql_num = 0;
var data = await curs.execute("select title, content from documents where namespace = '문서' order by length(content) desc limit ?, '122'", [sql_num]);
var content = `
내용이 긴 문서 (문서 이름공간, 리다이렉트 제외)
${navbtn(0, 0, 0, 0)}
`;
for(var i of data) {
if(i.content.match(/^[#]redirect\s(.*)\n$/)) continue;
content += '- <a href="/w/' + encodeURIComponent(i['title']) + '">' + html.escape(i['title']) + `</a> (${i.content.length}글자)
`;
}
content += '
' + navbtn(0, 0, 0, 0);
res.send(await render(req, '내용이 긴 문서', content, {}));
});
wiki.all(/^\/admin\/config$/, async(req, res, next) => {
if(!['POST', 'GET'].includes(req.method)) return next();
if(!islogin(req)) return res.status(403).send(await showError(req, 'permission'));
if(!((hostconfig.owners || []).includes(ip_check(req)))) {
return res.status(403).send(await showError(req, 'permission'));
}
const defskin = config.getString('wiki.default_skin', hostconfig.skin);
var skopt = ;
for(var skin of skinList) {
var opt = `<option value="${skin}" ${config.getString('wiki.default_skin', hostconfig.skin) == skin ? 'selected' : }>${skin}</option>`;
skopt += opt;
}
var filterd = await curs.execute("select address from email_filters");
var filters = [];
for(var item of filterd) {
filters.push(item.address);
}
// 실제 더시드 UI가 밝혀지길...
var content = `
<form method=post class=settings-section>
<label class=control-label>위키 이름</label>
<input class=form-control type=text name=wiki.site_name value="${html.escape(config.getString('wiki.site_name', '더 시드'))}" />
<label class=control-label>대문</label>
<input class=form-control type=text name=wiki.front_page value="${html.escape(config.getString('wiki.front_page', 'FrontPage'))}" />
<label class=control-label>기본 스킨</label>
<select class=form-control name=wiki.default_skin>
${skopt}
</select>
<label class=control-label>이메일 허용 목록 활성화</label>
<label>
<input type=checkbox name=wiki.email_filter_enabled value=true${config.getString('wiki.email_filter_enabled', 'false') == 'true' ? ' checked' : } />
사용
</label>
<label class=control-label>이메일 허용 목록</label>
<input class=form-control type=text name=filters value="${html.escape(filters.join(';'))}" />
<label class=control-label>공지</label>
<input class=form-control type=text name=wiki.sitenotice value="${html.escape(config.getString('wiki.sitenotice', ))}" />
<label class=control-label>편집 안내</label>
<input class=form-control type=text name=wiki.editagree_text value="${html.escape(config.getString('wiki.editagree_text', `문서 편집을 저장하면 당신은 기여한 내용을 CC-BY-NC-SA 2.0 KR으로 배포하고 기여한 문서에 대한 하이퍼링크나 URL을 이용하여 저작자 표시를 하는 것으로 충분하다는 데 동의하는 것입니다. 이 동의는 철회할 수 없습니다.`))}" />
<label class=control-label>사이트 주소</label>
<input class=form-control type=text name=wiki.canonical_url value="${html.escape(config.getString('wiki.canonical_url', ))}" />
<label class=control-label>라이선스 주소</label>
<input class=form-control type=text name=wiki.copyright_url value="${html.escape(config.getString('wiki.copyright_url', ))}" />
<label class=control-label>저작권 안내 문구</label>
<input class=form-control type=text name=wiki.copyright_text value="${html.escape(config.getString('wiki.copyright_text', ))}" />
<label class=control-label>하단 문구</label>
<input class=form-control type=text name=wiki.footer_text value="${html.escape(config.getString('wiki.footer_text', ))}" />
<label class=control-label>로고 주소</label>
<input class=form-control type=text name=wiki.logo_url value="${html.escape(config.getString('wiki.logo_url', ))}" />
<label class=control-label>사용자정의 이름공간</label>
<input class=form-control type=text name=custom_namespaces value="${html.escape((hostconfig.custom_namespaces || []).join(';'))}" />
<button type=submit style="width: 100px;" class="btn btn-primary">저장</button>
</form>
`;
if(req.method == 'POST') {
if(wikiconfig['wiki.site_name'] != req.body['wiki.site_name']) {
curs.execute("update documents set namespace = ? where namespace = ?", [req.body['wiki.site_name'], wikiconfig['wiki.site_name']]);
curs.execute("update history set namespace = ? where namespace = ?", [req.body['wiki.site_name'], wikiconfig['wiki.site_name']]);
curs.execute("update threads set namespace = ? where namespace = ?", [req.body['wiki.site_name'], wikiconfig['wiki.site_name']]);
curs.execute("update edit_requests set namespace = ? where namespace = ?", [req.body['wiki.site_name'], wikiconfig['wiki.site_name']]);
curs.execute("update acl set namespace = ? where namespace = ?", [req.body['wiki.site_name'], wikiconfig['wiki.site_name']]);
curs.execute("update classic_acl set namespace = ? where namespace = ?", [req.body['wiki.site_name'], wikiconfig['wiki.site_name']]);
}
if(!req.body['wiki.email_filter_enabled'])
req.body['wiki.email_filter_enabled'] = 'false';
if(req.body['custom_namespaces'])
hostconfig.custom_namespaces = req.body['custom_namespaces'].split(';').map(item => item.replace(/(^(\s+)|(\s+)$)/g, )).filter(item => item);
if(req.body['filters']) {
await curs.execute("delete from email_filters");
for(var f of req.body['filters'].split(';').map(item => item.replace(/(^(\s+)|(\s+)$)/g, )).filter(item => item)) {
curs.execute("insert into email_filters (address) values (?)", [f]);
}
}
for(var item of ['wiki.site_name', 'wiki.front_page', 'wiki.default_skin', 'filters', 'wiki.sitenotice', 'wiki.editagree_text', 'wiki.canonical_url', 'wiki.copyright_url', 'wiki.copyright_text', 'wiki.footer_text', 'wiki.logo_url']) {
wikiconfig[item] = req.body[item];
await curs.execute("delete from config where key = ?", [item]);
await curs.execute("insert into config (key, value) values (?, ?)", [item, wikiconfig[item]]);
}
fs.writeFile('config.json', JSON.stringify(hostconfig), 'utf8', () => 1);
return res.redirect('/admin/config');
}
return res.send(await render(req, '환경설정', content));
});
// 역링크 초기화 (디버그 전용)
if(hostconfig.debug) wiki.get('/ResetXref', function(req, res) {
print('기존 역링크 데이타 삭제');
curs.execute("delete from backlink")
.then(() => {
print('문서 목록 불러오기');
curs.execute("select title, namespace, content from documents")
.then(async dbdocs => {
print('초기화 시작...');
for(var item of dbdocs) {
prt(totitle(item.title, item.namespace) + ' 처리 중... ');
await markdown(item.content, 0, totitle(item.title, item.namespace) + , 'backlinkinit');
print('완료!');
}
print('모두 처리 완료.');
return res.send('완료!');
});
});
});
// 404 페이지
wiki.use(function(req, res, next) {
return res.status(404).send(`
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<meta name=viewport content="width=1240">
<title>Page is not found!</title>
<style>
section {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 80px 0 0;
background-color:#EFEFEF;
font-family: "Open Sans", sans-serif;
text-align: center;
}
h1 {
margin: 0 0 19px;
font-size: 40px;
font-weight: normal;
color: #E02B2B;
line-height: 40px;
}
p {
margin: 0 0 57px;
font-size: 16px;
color:#444;
line-height: 23px;
}
</style>
</head>
<body>
<section>
404
Page is not found!
<a href="/">Back to home</a>
</section>
</body>
</html>
`);
});
(async function setWikiData() {
// 위키 설정 캐시
var data = await curs.execute("select key, value from config");
for(var cfg of data) {
wikiconfig[cfg.key] = cfg.value;
}
// 권한 캐시
var data = await curs.execute("select username, perm from perms order by username");
for(var prm of data) {
if(typeof(permlist[prm.username]) == 'undefined')
permlist[prm.username] = [prm.perm];
else
permlist[prm.username].push(prm.perm);
}
// 사용자 설정 캐시
var data = await curs.execute("select username, key, value from user_settings");
for(var set of data) {
if(!userset[set.username]) userset[set.username] = {};
userset[set.username][set.key] = set.value;
}
// 엔진 업그레이드
switch(Number(config.getString('update_code', '1'))) {
case 1: {
// 역링크, 4.2.0 미만용 ACL
try {
await curs.execute("create table backlink (title text default , namespace text default , link text default , linkns text default , type text default 'link')");
await curs.execute("create table classic_acl (title text default , namespace text default , blockkorea text default , blockbot text default , read text default , edit text default , del text default , discuss text default , move text default )");
} catch(e) {}
} case 2: {
// 역링크 테이블에 문서 존재 여부 열 추가
try {
await curs.execute("alter table backlink\nADD exist text;");
} catch(e) {}
} case 3: {
// 문서 테이블에 최종수정일 열 추가
try {
await curs.execute("alter table documents\nADD time text;");
(async function() {
for(let item of (await curs.execute("select title, namespace from documents"))) {
const d = await curs.execute("select time from history where title = ? and namespace = ? order by cast(rev as integer) desc limit 1", [item.title, item.namespace]);
if(!d.length) continue;
await curs.execute("update documents set time = ? where title = ? and namespace = ?", [d[0].time, item.title, item.namespace]);
}
})();
} catch(e) {}
} case 4: {
// 탈퇴한 사용자
try {
await curs.execute("update res set username = '탈퇴한 사용자' where username = and ismember = 'author'");
await curs.execute("update history set username = '탈퇴한 사용자' where username = and ismember = 'author'");
} catch(e) {}
} case 5: {
// 자동 로그인 구현
try {
await curs.execute("create table autologin_tokens ( username text default , token text default )");
await curs.execute("create table trusted_devices ( username text default , id text default )");
} catch(e) {}
} case 6: {
// 로그인 내역 테이블 빼먹음
try {
await curs.execute("alter table login_history\nADD time text;");
} catch(e) {}
} case 7: {
// 위키 설정
try {
const fd = await curs.execute("select value from config where key = 'frontpage'");
if(fd.length && fd[0].value) {
wikiconfig.front_page = fd[0].value;
delete wikiconfig.frontpage;
await curs.execute("delete from config where key = 'frontpage' or key = 'front_page'");
await curs.execute("insert into config (key, value) values ('front_page', ?)", [fd[0].value]);
}
const cn = await curs.execute("select value from config where key = 'copyright_notice'");
if(cn.length && cn[0].value) {
wikiconfig.editagree_text = cn[0].value;
delete wikiconfig.copyright_notice;
await curs.execute("delete from config where key = 'copyright_notice'");
await curs.execute("insert into config (key, value) values ('editagree_text', ?)", [cn[0].value]);
}
for(var key in wikiconfig) {
if(key == 'update_code') continue;
await curs.execute("delete from config where key = ?", [key]);
await curs.execute("insert into config (key, value) values (?, ?)", ['wiki.' + key, wikiconfig[key]]);
wikiconfig['wiki.' + key] = wikiconfig[key];
delete wikiconfig[key];
}
} catch(e) {}
} case 8: {
// 탈퇴한 사용자 2
curs.execute("update history set username = '탈퇴한 사용자', ismember = 'ip' where username = '탈퇴한 사용자' and ismember = 'author'");
curs.execute("update res set username = '탈퇴한 사용자', ismember = 'ip' where username = '탈퇴한 사용자' and ismember = 'author'");
curs.execute("update block_history set executer = '탈퇴한 사용자', ismember = 'ip' where executer = '탈퇴한 사용자' and ismember = 'author'");
curs.execute("update edit_requests set processor = '탈퇴한 사용자', ismember = 'ip' where processor = '탈퇴한 사용자' and ismember = 'author'");
curs.execute("update edit_requests set username = '탈퇴한 사용자', ismember = 'ip' where username = '탈퇴한 사용자' and ismember = 'author'");
} case 9: {
// 구버전 더시드 토론
try {
await curs.execute("alter table threads\nADD num text;");
let dd = await curs.execute("select tnum from threads");
for(var idx=0; idx<dd.length; idx++) {
let item = dd[idx];
let dt = await curs.execute("select time from res where id = '1' and tnum = ?", [item.tnum]);
dd[idx].tt = Number(dt[0].time);
}
dd = dd.sort((l, r) => l.tt - r.tt);
for(var idx=0; idx<dd.length; idx++) {
let item = dd[idx];
await curs.execute("update threads set num = ? where tnum = ?", [String(idx + 1), item.tnum]);
}
} catch(e) {}
} case 10: {
// 새로운 토론주소
try {
await curs.execute("alter table threads\nADD slug text;");
await curs.execute("alter table edit_requests\nADD slug text;");
var dd = await curs.execute("select tnum from threads");
for(let item of dd) {
await curs.execute("update threads set slug = ? where tnum = ?", [newID(), item.tnum]);
}
var dd = await curs.execute("select id from edit_requests");
for(let item of dd) {
await curs.execute("update edit_requests set slug = ? where id = ?", [newID(), item.id]);
}
} catch(e) {}
} case 11: {
// 까먹음
try {
await curs.execute("alter table res\nADD slug text;");
var dd = await curs.execute("select tnum from threads");
for(let item of dd) {
await curs.execute("update res set slug = ? where tnum = ?", [newID(), item.tnum]);
}
} catch(e) {}
}
}
await curs.execute("update config set value = ? where key = 'update_code'", [updatecode]);
wikiconfig.update_code = updatecode;
if(hostconfig.debug) print('경고! 위키가 디버그 모드에서 실행 중입니다. 알려지지 않은 취약점에 노출될 수 있습니다.\n');
// 작성이 필요한 문서
async function cacheNeededPages() {
neededPages = {};
for(var ns of fetchNamespaces()) {
neededPages[ns] = [];
var data = await curs.execute("select distinct link from backlink where exist = '0' and linkns = ?", [ns]);
for(let i of data) {
neededPages[ns].push(i.link);
}
}
}
setInterval(cacheNeededPages, 86400000);
cacheNeededPages();
// 서버실행
const { host, port } = hostconfig;
if(hostconfig.default_host)
wiki.listen(process.env.PORT);
else
wiki.listen(port, host);
print(host + (port == 80 ? : (':' + port)) + '에서 실행 중. . .');
beep();
if(hostconfig.search_autostart) {
child_process.execFile('node', ['search.js'], function() {});
}
})();
if(hostconfig.self_request) {
var rq = setInterval(function() {
https.request({
host: hostconfig.self_request,
path: '/RecentDiscuss',
headers: {
"Cookie": 'a=1; korori=a; ',
"Host": hostconfig.self_request,
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36",
},
port: 443,
}, function(res) {
var ret = ;
res.on('data', function(chunk) {
ret += chunk;
});
res.on('end', function() {
});
}).end();
}, (50 + Math.floor(Math.random() * 10)) * 1000);
}
}