From e34ea592bd9981d99d1c7f70c0fbf10c0bd83eb3 Mon Sep 17 00:00:00 2001 From: neptune Date: Fri, 12 Nov 2021 01:15:38 -0300 Subject: Actualización inicial a Python 3 --- cgi/anarkia.py | 34 +-- cgi/anarkia.py.orig | 489 +++++++++++++++++++++++++++++++++ cgi/anarkia.py.rej | 42 +++ cgi/api.py | 58 ++-- cgi/database.py | 46 ++-- cgi/formatting.py | 76 +++-- cgi/framework.py | 96 +++---- cgi/img.py | 40 ++- cgi/manage.py | 285 +++++++++---------- cgi/modapi.py | 32 +-- cgi/oekaki.py | 19 +- cgi/post.py | 226 +++++++-------- cgi/template.py | 18 +- cgi/templates/home.html | 5 +- cgi/templates/manage/boardoptions.html | 34 +-- cgi/templates/manage/ipshow.html | 10 +- cgi/templates/revision.html | 2 +- cgi/templates/txt_board.html | 6 +- cgi/templates/txt_thread.html | 12 +- cgi/weabot.py | 235 ++++++++-------- 20 files changed, 1126 insertions(+), 639 deletions(-) create mode 100644 cgi/anarkia.py.orig create mode 100644 cgi/anarkia.py.rej diff --git a/cgi/anarkia.py b/cgi/anarkia.py index de9152f..ad64596 100644 --- a/cgi/anarkia.py +++ b/cgi/anarkia.py @@ -18,7 +18,7 @@ def anarkia(self, path_split): self.output = main() return - raise UserError, 'Ya fue, baisano...' + raise UserError('Ya fue, baisano...') if path_split[2] == 'opt': self.output = boardoptions(self.formdata) @@ -33,7 +33,7 @@ def anarkia(self, path_split): elif path_split[2] == 'emojis': self.output = emojis(self.formdata) else: - raise UserError, 'ke?' + raise UserError('ke?') def main(): @@ -90,7 +90,7 @@ def type(formdata): logAction(msg) return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) else: - raise UserError, 'Esta acción sólo se puede realizar cada 10 minutos. Faltan: %d mins.' % (10-int(dif/60)) + raise UserError('Esta acción sólo se puede realizar cada 10 minutos. Faltan: %d mins.' % (10-int(dif/60))) return renderTemplate('anarkia.html', {'mode': 7, 'type_now': type_now, 'type_do': type_do}) @@ -203,9 +203,9 @@ def bans(formdata): ban = FetchOne("SELECT * FROM `bans` WHERE id = %d" % unban) if not ban: - raise UserError, "Ban inválido." + raise UserError("Ban inválido.") if ban['boards'] != boardpickle: - raise USerError, "Ban inválido." + raise USerError("Ban inválido.") UpdateDb('DELETE FROM `bans` WHERE id = %s' % ban['id']) logAction("Usuario %s desbaneado." % ban['ip'][:4]) @@ -255,7 +255,7 @@ def mod(formdata): logAction(msg) return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) else: - raise UserError, "jaj no" + raise UserError("jaj no") elif formdata.get('restore'): postid = int(formdata['restore']) post = FetchOne('SELECT id, parentid FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % ( @@ -293,7 +293,7 @@ def mod(formdata): ban = FetchOne("SELECT `id` FROM `bans` WHERE `ip` = '" + post['ip'] + "' AND `boards` = '" + _mysql.escape_string(where) + "' LIMIT 1") if ban: - raise UserError, "Este usuario ya esta baneado." + raise UserError("Este usuario ya esta baneado.") # Blind mode if formdata.get('blind') == '1': @@ -339,23 +339,23 @@ def boardoptions(formdata): board['subject'] = formdata['subject'].replace('script', '') board['message'] = formdata['message'].replace('script', '') board['useid'] = formdata['useid'] - if 'disable_name' in formdata.keys(): + if 'disable_name' in formdata: board['disable_name'] = '1' else: board['disable_name'] = '0' - if 'disable_subject' in formdata.keys(): + if 'disable_subject' in formdata: board['disable_subject'] = '1' else: board['disable_subject'] = '0' - if 'allow_noimage' in formdata.keys(): + if 'allow_noimage' in formdata: board['allow_noimage'] = '1' else: board['allow_noimage'] = '0' - if 'allow_images' in formdata.keys(): + if 'allow_images' in formdata: board['allow_images'] = '1' else: board['allow_images'] = '0' - if 'allow_image_replies' in formdata.keys(): + if 'allow_image_replies' in formdata: board['allow_image_replies'] = '1' else: board['allow_image_replies'] = '0' @@ -364,7 +364,7 @@ def boardoptions(formdata): UpdateDb("DELETE FROM `boards_filetypes` WHERE `boardid` = %s" % board['id']) for filetype in filetypelist(): - if 'filetype'+filetype['ext'] in formdata.keys(): + if 'filetype'+filetype['ext'] in formdata: UpdateDb("INSERT INTO `boards_filetypes` VALUES (%s, %s)" % (board['id'], filetype['id'])) @@ -373,28 +373,28 @@ def boardoptions(formdata): if board['maxsize'] > 10000: board['maxsize'] = 10000 except: - raise UserError, _("Max size must be numeric.") + raise UserError(_("Max size must be numeric.")) try: board['thumb_px'] = int(formdata['thumb_px']) if board['thumb_px'] > 500: board['thumb_px'] = 500 except: - raise UserError, _("Max thumb dimensions must be numeric.") + raise UserError(_("Max thumb dimensions must be numeric.")) try: board['numthreads'] = int(formdata['numthreads']) if board['numthreads'] > 15: board['numthreads'] = 15 except: - raise UserError, _("Max threads shown must be numeric.") + raise UserError(_("Max threads shown must be numeric.")) try: board['numcont'] = int(formdata['numcont']) if board['numcont'] > 15: board['numcont'] = 15 except: - raise UserError, _("Max replies shown must be numeric.") + raise UserError(_("Max replies shown must be numeric.")) t = time.time() updateBoardSettings() diff --git a/cgi/anarkia.py.orig b/cgi/anarkia.py.orig new file mode 100644 index 0000000..de9152f --- /dev/null +++ b/cgi/anarkia.py.orig @@ -0,0 +1,489 @@ +# coding=utf-8 +import _mysql +from database import * +from framework import * +from template import * +from img import * +from post import * +from settings import Settings + +d_thread = {} +d_post = {} + + +def anarkia(self, path_split): + setBoard('anarkia') + + if len(path_split) <= 2: + self.output = main() + return + + raise UserError, 'Ya fue, baisano...' + + if path_split[2] == 'opt': + self.output = boardoptions(self.formdata) + elif path_split[2] == 'mod': + self.output = mod(self.formdata) + elif path_split[2] == 'bans': + self.output = bans(self.formdata) + elif path_split[2] == 'css': + self.output = css(self.formdata) + elif path_split[2] == 'type': + self.output = type(self.formdata) + elif path_split[2] == 'emojis': + self.output = emojis(self.formdata) + else: + raise UserError, 'ke?' + + +def main(): + board = Settings._.BOARD + + logs = FetchAll( + "SELECT * FROM `logs` WHERE `staff` = 'Anarko' ORDER BY `timestamp` DESC") + for log in logs: + log['timestamp_formatted'] = formatTimestamp(log['timestamp']) + + return renderTemplate('anarkia.html', {'mode': 0, 'logs': logs}) + + +def type(formdata): + board = Settings._.BOARD + + if board['board_type'] == '1': + (type_now, type_do, do_num) = ('BBS', 'IB', '0') + else: + (type_now, type_do, do_num) = ('IB', 'BBS', '1') + + if formdata.get('transform') == 'do': + t = 0 + try: + with open('anarkia_time') as f: + t = int(f.read()) + except IOError: + pass + + dif = time.time() - t + if dif > (10 * 60): + # if True: + import re + t = time.time() + + board['board_type'] = do_num + board['force_css'] = Settings.HOME_URL + \ + 'anarkia/style_' + type_do.lower() + '.css' + updateBoardSettings() + + # update posts + fix_board() + + # regenerate + setBoard('anarkia') + regenerateBoard(True) + + tf = timeTaken(t, time.time()) + + with open('anarkia_time', 'w') as f: + t = f.write(str(int(time.time()))) + + msg = 'Cambiada estructura de sección a %s. (%s)' % (type_do, tf) + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + raise UserError, 'Esta acción sólo se puede realizar cada 10 minutos. Faltan: %d mins.' % (10-int(dif/60)) + + return renderTemplate('anarkia.html', {'mode': 7, 'type_now': type_now, 'type_do': type_do}) + + +def fix_board(): + board = Settings._.BOARD + get_fix_dictionary() + + if board['board_type'] == '1': + to_fix = FetchAll( + "SELECT * FROM posts WHERE message LIKE '%%anarkia/res/%%' AND boardid = %s" % board['id']) + else: + to_fix = FetchAll( + "SELECT * FROM posts WHERE message LIKE '%%anarkia/read/%%' AND boardid = %s" % board['id']) + + for p in to_fix: + try: + if board['board_type'] == '1': + newmessage = re.sub( + r'/anarkia/res/(\d+).html#(\d+)">>>(\d+)', fix_to_bbs, p['message']) + else: + newmessage = re.sub( + r'/anarkia/read/(\d+)/(\d+)">>>(\d+)', fix_to_ib, p['message']) + + UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % + (_mysql.escape_string(newmessage), board['id'], p['id'])) + except KeyError: + pass + + return True + + +def fix_to_bbs(matchobj): + threadid = matchobj.group(1) + pid = matchobj.group(2) + new_thread = d_thread[threadid] + new_post = d_post[new_thread][pid] + return '/anarkia/read/%s/%s">>>%s' % (new_thread, new_post, new_post) + + +def fix_to_ib(matchobj): + threadid = matchobj.group(1) + num = int(matchobj.group(2)) + new_thread = d_thread[threadid] + new_post = d_post[new_thread][num] + return '/anarkia/res/%s.html#%s">>>%s' % (new_thread, new_post, new_post) + + +def get_fix_dictionary(): + global d_thread, d_post + board = Settings._.BOARD + res = FetchAll( + "SELECT id, timestamp, parentid FROM posts WHERE boardid = %s ORDER BY CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC" % board['id']) + num = 1 + thread = 0 + for p in res: + pid = p['id'] + if p['parentid'] == '0': + num = 1 + + time = p['timestamp'] + if board['board_type'] == '1': + d_thread[pid] = time + thread = time + else: + d_thread[time] = pid + thread = pid + + d_post[thread] = {} + + if board['board_type'] == '1': + d_post[thread][pid] = num + else: + d_post[thread][num] = pid + num += 1 + + return + + +def css(formdata): + board = Settings._.BOARD + + if board['board_type'] == '1': + basename = 'style_bbs.css' + else: + basename = 'style_ib.css' + + fname = '%sanarkia/%s' % (Settings.HOME_DIR, basename) + + if formdata.get('cssfile'): + with open(fname, 'w') as f: + cssfile = f.write(formdata['cssfile']) + + msg = 'CSS actualizado.' + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + + with open(fname) as f: + cssfile = f.read() + + return renderTemplate('anarkia.html', {'mode': 6, 'basename': basename, 'cssfile': cssfile}) + + +def bans(formdata): + board = Settings._.BOARD + + if formdata.get('unban'): + unban = int(formdata['unban']) + boardpickle = pickle.dumps(['anarkia']) + + ban = FetchOne("SELECT * FROM `bans` WHERE id = %d" % unban) + if not ban: + raise UserError, "Ban inválido." + if ban['boards'] != boardpickle: + raise USerError, "Ban inválido." + + UpdateDb('DELETE FROM `bans` WHERE id = %s' % ban['id']) + logAction("Usuario %s desbaneado." % ban['ip'][:4]) + regenerateAccess() + + bans = FetchAll('SELECT * FROM `bans` WHERE staff = \'anarko\'') + for ban in bans: + ban['added'] = formatTimestamp(ban['added']) + if ban['until'] == '0': + ban['until'] = _('Does not expire') + else: + ban['until'] = formatTimestamp(ban['until']) + return renderTemplate('anarkia.html', {'mode': 5, 'bans': bans}) + + +def mod(formdata): + board = Settings._.BOARD + + if formdata.get('thread'): + parentid = int(formdata['thread']) + posts = FetchAll('SELECT * FROM `posts` WHERE (parentid = %d OR id = %d) AND boardid = %s ORDER BY `id` ASC' % + (parentid, parentid, board['id'])) + return renderTemplate('anarkia.html', {'mode': 3, 'posts': posts}) + elif formdata.get('lock'): + postid = int(formdata['lock']) + post = FetchOne('SELECT id, locked FROM posts WHERE boardid = %s AND id = %d AND parentid = 0 LIMIT 1' % ( + board['id'], postid)) + if post['locked'] == '0': + setLocked = 1 + msg = "Hilo %s cerrado." % post['id'] + else: + setLocked = 0 + msg = "Hilo %s abierto." % post['id'] + + UpdateDb("UPDATE `posts` SET `locked` = %d WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % ( + setLocked, board["id"], post["id"])) + threadUpdated(post['id']) + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + elif formdata.get('del'): + postid = int(formdata['del']) + post = FetchOne('SELECT id, parentid FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % ( + board['id'], postid)) + if post['parentid'] != '0': + deletePost(post['id'], None, '3', False) + msg = "Mensaje %s eliminado." % post['id'] + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + raise UserError, "jaj no" + elif formdata.get('restore'): + postid = int(formdata['restore']) + post = FetchOne('SELECT id, parentid FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % ( + board['id'], postid)) + + UpdateDb('UPDATE `posts` SET `IS_DELETED` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1' % ( + board['id'], post['id'])) + if post['parentid'] != '0': + threadUpdated(post['parentid']) + else: + regenerateFrontPages() + msg = "Mensaje %s recuperado." % post['id'] + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + elif formdata.get('ban'): + postid = int(formdata['ban']) + post = FetchOne('SELECT id, ip FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % ( + board['id'], postid)) + + return renderTemplate('anarkia.html', {'mode': 4, 'post': post}) + elif formdata.get('banto'): + postid = int(formdata['banto']) + post = FetchOne('SELECT id, message, parentid, ip FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % ( + board['id'], postid)) + + reason = formdata.get('reason').replace( + 'script', '').replace('meta', '') + if reason is not None: + if formdata['seconds'] != '0': + until = str(timestamp() + int(formdata['seconds'])) + else: + until = '0' + where = pickle.dumps(['anarkia']) + + ban = FetchOne("SELECT `id` FROM `bans` WHERE `ip` = '" + + post['ip'] + "' AND `boards` = '" + _mysql.escape_string(where) + "' LIMIT 1") + if ban: + raise UserError, "Este usuario ya esta baneado." + + # Blind mode + if formdata.get('blind') == '1': + blind = '1' + else: + blind = '0' + + InsertDb("INSERT INTO `bans` (`ip`, `netmask`, `boards`, `added`, `until`, `staff`, `reason`, `blind`) VALUES ('" + post['ip'] + "', INET_ATON('255.255.255.255'), '" + _mysql.escape_string( + where) + "', " + str(timestamp()) + ", " + until + ", 'anarko', '" + _mysql.escape_string(formdata['reason']) + "', '"+blind+"')") + + newmessage = post['message'] + \ + '
A este usuario se le revocó el acceso. Razón: %s' % reason + + UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % ( + _mysql.escape_string(newmessage), board['id'], post['id'])) + if post['parentid'] != '0': + threadUpdated(post['parentid']) + else: + regenerateFrontPages() + regenerateAccess() + + msg = "Usuario %s baneado." % post['ip'][:4] + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + reports = FetchAll("SELECT * FROM `reports` WHERE board = 'anarkia'") + threads = FetchAll( + 'SELECT * FROM `posts` WHERE boardid = %s AND parentid = 0 ORDER BY `bumped` DESC' % board['id']) + return renderTemplate('anarkia.html', {'mode': 2, 'threads': threads, 'reports': reports}) + + +def boardoptions(formdata): + board = Settings._.BOARD + + if formdata.get('longname'): + # submitted + board['longname'] = formdata['longname'].replace('script', '') + board['postarea_desc'] = formdata['postarea_desc'].replace( + 'script', '').replace('meta', '') + board['postarea_extra'] = formdata['postarea_extra'].replace( + 'script', '').replace('meta', '') + board['anonymous'] = formdata['anonymous'].replace('script', '') + board['subject'] = formdata['subject'].replace('script', '') + board['message'] = formdata['message'].replace('script', '') + board['useid'] = formdata['useid'] + if 'disable_name' in formdata.keys(): + board['disable_name'] = '1' + else: + board['disable_name'] = '0' + if 'disable_subject' in formdata.keys(): + board['disable_subject'] = '1' + else: + board['disable_subject'] = '0' + if 'allow_noimage' in formdata.keys(): + board['allow_noimage'] = '1' + else: + board['allow_noimage'] = '0' + if 'allow_images' in formdata.keys(): + board['allow_images'] = '1' + else: + board['allow_images'] = '0' + if 'allow_image_replies' in formdata.keys(): + board['allow_image_replies'] = '1' + else: + board['allow_image_replies'] = '0' + + # Update file types + UpdateDb("DELETE FROM `boards_filetypes` WHERE `boardid` = %s" % + board['id']) + for filetype in filetypelist(): + if 'filetype'+filetype['ext'] in formdata.keys(): + UpdateDb("INSERT INTO `boards_filetypes` VALUES (%s, %s)" % + (board['id'], filetype['id'])) + + try: + board['maxsize'] = int(formdata['maxsize']) + if board['maxsize'] > 10000: + board['maxsize'] = 10000 + except: + raise UserError, _("Max size must be numeric.") + + try: + board['thumb_px'] = int(formdata['thumb_px']) + if board['thumb_px'] > 500: + board['thumb_px'] = 500 + except: + raise UserError, _("Max thumb dimensions must be numeric.") + + try: + board['numthreads'] = int(formdata['numthreads']) + if board['numthreads'] > 15: + board['numthreads'] = 15 + except: + raise UserError, _("Max threads shown must be numeric.") + + try: + board['numcont'] = int(formdata['numcont']) + if board['numcont'] > 15: + board['numcont'] = 15 + except: + raise UserError, _("Max replies shown must be numeric.") + + t = time.time() + updateBoardSettings() + setBoard('anarkia') + regenerateBoard(True) + tf = timeTaken(t, time.time()) + + msg = 'Opciones cambiadas. %s' % tf + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + else: + return renderTemplate('anarkia.html', {'mode': 1, 'boardopts': board, 'filetypes': filetypelist(), 'supported_filetypes': board['filetypes_ext']}) + + +def emojis(formdata): + board = Settings._.BOARD + board_pickle = _mysql.escape_string(pickle.dumps([board['dir']])) + + if formdata.get('new'): + import re + ext = {'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/png': 'png'} + + if not formdata['name']: + raise UserError, 'Ingresa nombre.' + if not re.match(r"^[0-9a-zA-Z]+$", formdata['name']): + raise UserError, 'Nombre inválido; solo letras/números.' + + name = ":%s:" % formdata['name'][:15] + data = formdata['file'] + + if not data: + raise UserError, 'Ingresa imagen.' + + # check if it exists + already = FetchOne("SELECT 1 FROM `filters` WHERE `boards` = '%s' AND `from` = '%s'" % ( + board_pickle, _mysql.escape_string(name))) + if already: + raise UserError, 'Este emoji ya existe.' + + # get image information + content_type, width, height, size, extra = getImageInfo(data) + + if content_type not in ext.keys(): + raise UserError, 'Formato inválido.' + if width > 500 or height > 500: + raise UserError, 'Dimensiones muy altas.' + if size > 150000: + raise UserError, 'Tamaño muy grande.' + + # create file names + thumb_width, thumb_height = getThumbDimensions(width, height, 32) + + file_path = Settings.ROOT_DIR + \ + board["dir"] + "/e/" + formdata['name'][:15] + \ + '.' + ext[content_type] + file_url = Settings.BOARDS_URL + \ + board["dir"] + "/e/" + formdata['name'][:15] + \ + '.' + ext[content_type] + to_filter = '' % ( + file_url, thumb_width, thumb_height) + + # start processing image + args = [Settings.CONVERT_PATH, "-", "-limit", "thread", "1", "-resize", + "%dx%d" % (thumb_width, thumb_height), "-quality", "80", file_path] + p = subprocess.Popen(args, stdout=subprocess.PIPE, + stdin=subprocess.PIPE, stderr=subprocess.PIPE) + out = p.communicate(input=data)[0] + + # insert into DB + sql = "INSERT INTO `filters` (`boards`, `type`, `action`, `from`, `to`, `staff`, `added`) VALUES ('%s', 0, 1, '%s', '%s', 'Anarko', '%s')" % ( + board_pickle, _mysql.escape_string(name), _mysql.escape_string(to_filter), timestamp()) + UpdateDb(sql) + + msg = "Emoji %s agregado." % name + logAction(msg) + return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg}) + elif formdata.get('del'): + return renderTemplate('anarkia.html', {'mode': 99, 'msg': 'Del.'}) + else: + filters = FetchAll( + "SELECT * FROM `filters` WHERE `boards` = '%s' ORDER BY `added` DESC" % board_pickle) + return renderTemplate('anarkia.html', {'mode': 8, 'emojis': filters}) + + +def filetypelist(): + filetypes = FetchAll('SELECT * FROM `filetypes` ORDER BY `ext` ASC') + return filetypes + + +def logAction(action): + InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + + str(timestamp()) + ", 'Anarko', '" + _mysql.escape_string(action) + "')") diff --git a/cgi/anarkia.py.rej b/cgi/anarkia.py.rej new file mode 100644 index 0000000..162e98a --- /dev/null +++ b/cgi/anarkia.py.rej @@ -0,0 +1,42 @@ +--- anarkia.py (original) ++++ anarkia.py (refactored) +@@ -418,31 +418,31 @@ + ext = {'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/png': 'png'} + + if not formdata['name']: +- raise UserError, 'Ingresa nombre.' ++ raise UserError('Ingresa nombre.') + if not re.match(r"^[0-9a-zA-Z]+$", formdata['name']): +- raise UserError, 'Nombre inválido; solo letras/números.' ++ raise UserError('Nombre inválido; solo letras/números.') + + name = ":%s:" % formdata['name'][:15] + data = formdata['file'] + + if not data: +- raise UserError, 'Ingresa imagen.' ++ raise UserError('Ingresa imagen.') + + # check if it exists + already = FetchOne("SELECT 1 FROM `filters` WHERE `boards` = '%s' AND `from` = '%s'" % ( + board_pickle, _mysql.escape_string(name))) + if already: +- raise UserError, 'Este emoji ya existe.' ++ raise UserError('Este emoji ya existe.') + + # get image information + content_type, width, height, size, extra = getImageInfo(data) + +- if content_type not in ext.keys(): +- raise UserError, 'Formato inválido. ++ if content_type not in ext: ++ raise UserError('Formato inválido.') + if width > 500 or height > 500: +- raise UserError, 'Dimensiones muy altas.' ++ raise UserError('Dimensiones muy altas.') + if size > 150000: +- raise UserError, 'Tamaño muy grande.' ++ raise UserError('Tamaño muy grande.') + + # create file names + thumb_width, thumb_height = getThumbDimensions(width, height, 32) diff --git a/cgi/api.py b/cgi/api.py index 6861809..fc44d76 100644 --- a/cgi/api.py +++ b/cgi/api.py @@ -12,11 +12,11 @@ def api(self, path_split): if len(path_split) > 2: try: self.output = api_process(self, path_split) - except APIError, e: + except APIError as e: self.output = api_error("error", e.message) - except UserError, e: + except UserError as e: self.output = api_error("failed", e.message) - except Exception, e: + except Exception as e: import sys import traceback exc_type, exc_value, exc_traceback = sys.exc_info() @@ -37,7 +37,7 @@ def api_process(self, path_split): #bans = ['181.72.116.62'] bans = [] if ip in bans: - raise APIError, "You have been blacklisted." + raise APIError("You have been blacklisted.") # with open('../api_log.txt', 'a') as f: # logstr = "[%s] %s: %s\n" % (formatTimestamp(t), ip, repr(path_split)) @@ -66,16 +66,16 @@ def api_process(self, path_split): try: limit = int(data_limit) except ValueError: - raise APIError, "Limit must be numeric" + raise APIError("Limit must be numeric") if data_since: try: since = int(data_since) except ValueError: - raise APIError, "Since must be numeric" + raise APIError("Since must be numeric") if limit > 50: - raise APIError, "Maximum limit is 50" + raise APIError("Maximum limit is 50") sql = "SELECT posts.id, boards.dir, timestamp, timestamp_formatted, posts.name, tripcode, email, posts.subject, posts.message, file, file_size, image_height, image_width, thumb, thumb_width, thumb_height, parentid FROM posts INNER JOIN boards ON boardid = boards.id WHERE timestamp > %d AND IS_DELETED = 0 AND boards.secret = 0 ORDER BY timestamp DESC LIMIT %d" % ( since, limit) @@ -101,10 +101,10 @@ def api_process(self, path_split): try: limit = int(data_limit) except ValueError: - raise APIError, "Limit must be numeric" + raise APIError("Limit must be numeric") if limit > 50: - raise APIError, "Maximum limit is 50" + raise APIError("Maximum limit is 50") threads = getLastAge(limit) if threads[0]['bumped'] > int(data_time): @@ -121,28 +121,28 @@ def api_process(self, path_split): numreplies = 2 if not data_board: - raise APIError, "Missing parameters" + raise APIError("Missing parameters") if data_limit: try: limit = int(data_limit) except ValueError: - raise APIError, "Limit must be numeric" + raise APIError("Limit must be numeric") if data_offset: try: offset = int(data_offset) except ValueError: - raise APIError, "Offset must be numeric" + raise APIError("Offset must be numeric") if data_replies: try: numreplies = int(data_replies) except ValueError: - raise APIError, "Replies must be numeric" + raise APIError("Replies must be numeric") if data_replies and limit > 30: - raise APIError, "Maximum limit is 30" + raise APIError("Maximum limit is 30") board = setBoard(data_board) @@ -207,19 +207,19 @@ def api_process(self, path_split): limit = 1000 if not data_board or (not data_threadid and not data_threadts): - raise APIError, "Missing parameters" + raise APIError("Missing parameters") if data_limit: try: limit = int(data_limit) except ValueError: - raise APIError, "Limit must be numeric" + raise APIError("Limit must be numeric") if data_offset: try: offset = int(data_offset) except ValueError: - raise APIError, "Offset must be numeric" + raise APIError("Offset must be numeric") if data_striphtml: if int(data_striphtml) == 1: @@ -241,13 +241,13 @@ def api_process(self, path_split): pass if not search_val: - raise APIError, "No thread ID" + raise APIError("No thread ID") op_post = FetchOne("SELECT id, timestamp, subject, locked FROM posts WHERE `%s` = '%d' AND boardid = '%s' AND parentid = 0" % ( search_field, search_val, board["id"])) if not op_post: - raise APIError, "Not a thread" + raise APIError("Not a thread") values['id'] = int(op_post['id']) values['timestamp'] = int(op_post['timestamp']) @@ -297,7 +297,7 @@ def api_process(self, path_split): data_postnum = formdata.get('num') if not data_board and (not data_postid or (not data_postnum and not data_parentid)): - raise APIError, "Missing parameters" + raise APIError("Missing parameters") board = setBoard(data_board) postid = 0 @@ -308,13 +308,13 @@ def api_process(self, path_split): try: postid = int(data_postid) except ValueError: - raise APIError, "Post ID must be numeric" + raise APIError("Post ID must be numeric") post = FetchOne("SELECT id, parentid, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_width, image_height, thumb, thumb_width, thumb_height, IS_DELETED FROM posts WHERE `id`='%d' AND boardid='%s'" % ( postid, board["id"])) if not post: - raise APIError, "Post ID cannot be found" + raise APIError("Post ID cannot be found") values['posts'] = [] @@ -345,7 +345,7 @@ def api_process(self, path_split): data_password = formdata.get('password') if not data_board or not data_postid or not data_password: - raise APIError, "Missing parameters" + raise APIError("Missing parameters") imageonly = False board = setBoard(data_board) @@ -353,7 +353,7 @@ def api_process(self, path_split): try: postid = int(data_postid) except ValueError: - raise APIError, "Post ID must be numeric" + raise APIError("Post ID must be numeric") if data_imageonly and data_imageonly == 1: imageonly = True @@ -363,7 +363,7 @@ def api_process(self, path_split): boarddir = formdata.get('board') if not boarddir: - raise APIError, "Missing parameters" + raise APIError("Missing parameters") parent = formdata.get('parent') trap1 = formdata.get('name', '') @@ -378,7 +378,7 @@ def api_process(self, path_split): oek_file = formdata.get('oek_file') password = formdata.get('password', '') noimage = formdata.get('noimage') - mobile = ("mobile" in formdata.keys()) + mobile = ("mobile" in formdata) # call post function (post_url, ttaken, postid) = self.make_post(ip, boarddir, parent, trap1, trap2, name, @@ -394,10 +394,10 @@ def api_process(self, path_split): try: limit = int(data_limit) except ValueError: - raise APIError, "Limit must be numeric" + raise APIError("Limit must be numeric") if limit > 30: - raise APIError, "Maximum limit is 30" + raise APIError("Maximum limit is 30") threads = getNewThreads(limit) values['threads'] = threads @@ -419,7 +419,7 @@ def api_process(self, path_split): board['numthreads'] = int(board['numthreads']) board['maxsize'] = int(board['maxsize']) else: - raise APIError, "Invalid method" + raise APIError("Invalid method") values['time'] = int(t) #values['time_taken'] = time.time() - t diff --git a/cgi/database.py b/cgi/database.py index 9b2c1e7..819c253 100644 --- a/cgi/database.py +++ b/cgi/database.py @@ -1,7 +1,8 @@ # coding=utf-8 import threading -import _mysql +import MySQLdb +import MySQLdb.cursors from settings import Settings database_lock = threading.Lock() @@ -19,57 +20,58 @@ except ImportError: def OpenDb(): if Settings._.CONN is None: - Settings._.CONN = _mysql.connect(host=Settings.DATABASE_HOST, - user=Settings.DATABASE_USERNAME, - passwd=Settings.DATABASE_PASSWORD, - db=Settings.DATABASE_DB) + Settings._.CONN = MySQLdb.connect(host=Settings.DATABASE_HOST, + user=Settings.DATABASE_USERNAME, + passwd=Settings.DATABASE_PASSWORD, + db=Settings.DATABASE_DB, + cursorclass=MySQLdb.cursors.SSDictCursor) -def FetchAll(query, method=1): +def FetchAll(query, params=None): """ Query and fetch all results as a list """ db = Settings._.CONN - db.query(query) - r = db.use_result() - return r.fetch_row(0, method) + c = db.cursor() + c.execute(query, params) + return c.fetchall() -def FetchOne(query, method=1): +def FetchOne(query, params=None): """ Query and fetch only the first result """ db = Settings._.CONN - db.query(query) - r = db.use_result() - try: - return r.fetch_row(1, method)[0] - except: - return None + c = db.cursor() + c.execute(query, params) + return c.fetchone() -def UpdateDb(query): +def UpdateDb(query, params=None): """ Update the DB (UPDATE/DELETE) and return # of affected rows """ db = Settings._.CONN - db.query(query) - return db.affected_rows() + c = db.cursor() + c.execute(query, params) + return c.rowcount -def InsertDb(query): +def InsertDb(query, params=None): """ Insert into the DB and return the primary key of new row """ db = Settings._.CONN - db.query(query) - return db.insert_id() + c = db.cursor() + c.execute(query, params) + return c.lastrowid def CloseDb(): if Settings._.CONN is not None: Settings._.CONN.close() + Settings._.CONN = None diff --git a/cgi/formatting.py b/cgi/formatting.py index 8037257..96bb73b 100644 --- a/cgi/formatting.py +++ b/cgi/formatting.py @@ -1,11 +1,10 @@ # coding=utf-8 import string -import cgi +import html import os import re import pickle import time -import _mysql from database import * from framework import * @@ -24,7 +23,7 @@ def format_post(message, ip, parentid, parent_timestamp=0): # Escape any HTML if user is not using Markdown or HTML if not Settings.USE_HTML: - message = cgi.escape(message) + message = html.escape(message) # Strip text message = message.rstrip()[0:8000] @@ -63,9 +62,7 @@ def tripcode(name): return '', '' board = Settings._.BOARD - - name = name.decode('utf-8') - key = Settings.TRIP_CHAR.decode('utf-8') + key = Settings.TRIP_CHAR # if there's a trip (namepart, marker, trippart) = name.partition('#') @@ -89,23 +86,24 @@ def tripcode(name): # return it if we don't have a normal tripcode if trippart == '': - return namepart.encode('utf-8'), trip.encode('utf-8') + return namepart, trip # do normal tripcode from crypt import crypt - try: - trippart = trippart.encode("sjis", "ignore") - except: - pass + #try: + # trippart = trippart.encode("sjis", "ignore") + #except: + # pass - trippart = cleanString(trippart, True, True) + trippart = html.unescape(trippart) + trippart = html.escape(trippart, True) salt = re.sub(r"[^\.-z]", ".", (trippart + "H..")[1:3]) - salt = salt.translate(string.maketrans(r":;=?@[\]^_`", "ABDFGabcdef")) + salt = salt.translate(str.maketrans(r":;=?@[\]^_`", "ABDFGabcdef")) trip = key + crypt(trippart, salt)[-10:] + trip - return namepart.encode('utf-8'), trip.encode('utf-8') + return namepart, trip - return name.encode('utf-8'), '' + return name, '' def iphash(ip, post, t, useid, mobile, agent, cap_id, hide_end, has_countrycode): @@ -113,7 +111,7 @@ def iphash(ip, post, t, useid, mobile, agent, cap_id, hide_end, has_countrycode) if cap_id: id = cap_id - elif 'sage' in post['email'] and useid == '1': + elif post['email'] and useid: id = '???' elif ip == "127.0.0.1": id = '???' @@ -121,7 +119,8 @@ def iphash(ip, post, t, useid, mobile, agent, cap_id, hide_end, has_countrycode) day = int((current_t + (Settings.TIME_ZONE*3600)) / 86400) word = ',' + str(day) # IDs change every 24 hours word += ',' + str(t) # IDs vary depending on thread - id = hide_data(ip + word, 6, "id", Settings.SECRET) + + id = getb64(getMD5(ip + word + Settings.SECRET))[:8] if hide_end: id += '*' @@ -133,7 +132,7 @@ def iphash(ip, post, t, useid, mobile, agent, cap_id, hide_end, has_countrycode) id += 'a' elif 'iPhone' in agent: id += 'i' - elif useid == '3': + elif useid == 3: if 'Firefox' in agent: id += 'F' elif 'Safari' in agent and not 'Chrome' in agent: @@ -169,7 +168,7 @@ def iphash(ip, post, t, useid, mobile, agent, cap_id, hide_end, has_countrycode) def cleanString(string, escape=True, quote=False): string = string.strip() if escape: - string = cgi.escape(string, quote) + string = html.escape(string, quote) return string @@ -191,8 +190,7 @@ def videoThumbs(message): matches = __RE.finditer(message) if matches: import json - import urllib - import urllib2 + import urllib.request, urllib.parse, urllib.error v_ids = [] videos = {} @@ -206,7 +204,7 @@ def videoThumbs(message): 'url': match.group(1), } if len(v_ids) >= Settings.VIDEO_THUMBS_LIMIT: - raise UserError, "Has incluído muchos videos en tu mensaje. El máximo es %d." % Settings.VIDEO_THUMBS_LIMIT + raise UserError("Has incluído muchos videos en tu mensaje. El máximo es %d." % Settings.VIDEO_THUMBS_LIMIT) if videos: params = { @@ -215,8 +213,8 @@ def videoThumbs(message): 'id': ','.join(v_ids) } r_url = "https://www.googleapis.com/youtube/v3/videos?" + \ - urllib.urlencode(params) - res = urllib2.urlopen(r_url) + urllib.parse.urlencode(params) + res = urllib.request.urlopen(r_url) res_json = json.load(res) offset = 0 @@ -227,14 +225,14 @@ def videoThumbs(message): try: new_url = '%(title)s (%(secs)s)
%(channel)s

' \ - % {'title': item['snippet']['title'].encode('utf-8'), - 'channel': item['snippet']['channelTitle'].encode('utf-8'), - 'secs': parseIsoPeriod(item['contentDetails']['duration']).encode('utf-8'), + % {'title': item['snippet']['title'], + 'channel': item['snippet']['channelTitle'], + 'secs': parseIsoPeriod(item['contentDetails']['duration']), 'url': videos[v_id]['url'], - 'id': v_id.encode('utf-8'), - 'thumb': item['snippet']['thumbnails']['default']['url'].encode('utf-8'), } + 'id': v_id, + 'thumb': item['snippet']['thumbnails']['default']['url'], } except UnicodeDecodeError: - raise UserError, repr(v_id) + raise UserError(repr(v_id)) message = message[:start+offset] + new_url + message[end+offset:] offset += len(new_url) - (end-start) @@ -323,7 +321,7 @@ def close_html(message): message = message.encode('utf-8') soup = BeautifulSoup.BeautifulSoup(message) - return unicode(soup).replace(' ', '').encode('utf-8') + return str(soup).replace(' ', '').encode('utf-8') def sanitize_html(message, decode=True): @@ -353,7 +351,7 @@ def sanitize_html(message, decode=True): del tag[attr] # We export the soup into a correct XHTML string - string = unicode(soup).encode('utf-8') + string = str(soup).encode('utf-8') # We remove some anomalies we don't want string = string.replace('
', '
').replace(' ', '') @@ -374,11 +372,11 @@ def checkWordfilters(message, ip, board): "SELECT * FROM `filters` WHERE `type` = '0' ORDER BY `id` ASC") for wordfilter in wordfilters: if wordfilter["boards"] != "": - boards = pickle.loads(wordfilter["boards"]) + boards = pickle.loads(wordfilter["boards"].encode("utf-8")) if wordfilter["boards"] == "" or board in boards: if wordfilter['action'] == '0': if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None: - raise UserError, wordfilter['reason'] + raise UserError(wordfilter['reason']) elif wordfilter['action'] == '1': message = re.compile(wordfilter['from'], re.DOTALL | re.IGNORECASE).sub( wordfilter['to'], message) @@ -395,10 +393,10 @@ def checkWordfilters(message, ip, board): "', " + str(timestamp()) + ", " + until + ", 'System', '" + _mysql.escape_string(wordfilter['reason']) + "', 'Word Auto-ban', '"+_mysql.escape_string(wordfilter['blind'])+"')") regenerateAccess() - raise UserError, wordfilter['reason'] + raise UserError(wordfilter['reason']) elif wordfilter['action'] == '3': if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None: - raise UserError, '%s' % (wordfilter['redirect_time'], wordfilter['redirect_url'], wordfilter['reason']) + raise UserError('%s' % (wordfilter['redirect_time'], wordfilter['redirect_url'], wordfilter['reason'])) return message @@ -428,7 +426,7 @@ def checkNamefilters(name, tripcode, ip, board): if match: # do action if namefilter['action'] == '0': - raise UserError, namefilter['reason'] + raise UserError(namefilter['reason']) elif namefilter['action'] == '1': name = namefilter['to'] tripcode = '' @@ -445,7 +443,7 @@ def checkNamefilters(name, tripcode, ip, board): "', " + str(timestamp()) + ", " + until + ", 'System', '" + _mysql.escape_string(namefilter['reason']) + "', 'Name Auto-ban', '"+_mysql.escape_string(namefilter['blind'])+"')") regenerateAccess() - raise UserError, namefilter['reason'] + raise UserError(namefilter['reason']) elif namefilter['action'] == '3': - raise UserError, '%s' % (namefilter['redirect_time'], namefilter['redirect_url'], namefilter['reason']) + raise UserError('%s' % (namefilter['redirect_time'], namefilter['redirect_url'], namefilter['reason'])) return name, tripcode diff --git a/cgi/framework.py b/cgi/framework.py index faea8d9..5277df0 100644 --- a/cgi/framework.py +++ b/cgi/framework.py @@ -6,11 +6,11 @@ import time import hashlib import pickle import socket -import _mysql -import urllib +import urllib.request, urllib.parse, urllib.error import re import logging -from Cookie import SimpleCookie +import base64 +from http.cookies import SimpleCookie from settings import Settings from database import * @@ -22,14 +22,14 @@ def setBoard(dir): with the data from the db. """ if not dir: - raise UserError, _("The specified board is invalid.") + raise UserError(_("The specified board is invalid.")) logging.debug("Seteando el board " + dir) - board = FetchOne("SELECT * FROM `boards` WHERE `dir` = '%s' LIMIT 1" % _mysql.escape_string(dir)) + board = FetchOne("SELECT * FROM `boards` WHERE `dir` = %s LIMIT 1", (dir,)) if not board: - raise UserError, _("The specified board is invalid.") + raise UserError(_("The specified board is invalid.")) board["filetypes"] = FetchAll( - "SELECT * FROM `boards_filetypes` INNER JOIN `filetypes` ON filetypes.id = boards_filetypes.filetypeid WHERE `boardid` = %s ORDER BY `ext` ASC" % _mysql.escape_string(board['id'])) + "SELECT * FROM `boards_filetypes` INNER JOIN `filetypes` ON filetypes.id = boards_filetypes.filetypeid WHERE `boardid` = %s ORDER BY `ext` ASC", (board['id'],)) board["filetypes_ext"] = [filetype['ext'] for filetype in board['filetypes']] @@ -145,7 +145,7 @@ def updateBoardSettings(): del board["filetypes"] del board["filetypes_ext"] post_values = ["`" + _mysql.escape_string(str(key)) + "` = '" + _mysql.escape_string( - str(value)) + "'" for key, value in board.iteritems()] + str(value)) + "'" for key, value in board.items()] UpdateDb("UPDATE `boards` SET %s WHERE `id` = '%s' LIMIT 1" % (", ".join(post_values), board["id"])) @@ -242,20 +242,11 @@ def getFormData(self): self.environ["wsgi.post_form"] = post_form self.environ["wsgi.input"] = new_input - try: - formdata = {} - for key in dict(fs): - try: - formdata.update({key: fs[key].value}) - if key == "file": - formdata.update( - {"file_original": secure_filename(fs[key].filename)}) - except AttributeError: - formdata.update({key: fs[key]}) + formdata = {} + for key in dict(fs): + formdata.update({key: fs[key].value}) - return formdata - except TypeError: - return fs + return formdata class InputProcessed(object): @@ -273,45 +264,22 @@ def secure_filename(path): ''.join([os.path.sep, os.path.altsep or '']))) return cgi.escape(split.sub('', path)) - -def getMD5(data): +def getMD5b(data): m = hashlib.md5() m.update(data) - + return m.hexdigest() -def nullstr(len): return "\0" * len - +def getMD5(data): + m = hashlib.md5() + m.update(bytes(data, 'utf-8')) -def hide_data(data, length, key, secret): - """ - Encrypts data, useful for tripcodes and IDs - """ - crypt = rc4(nullstr(length), rc4( - nullstr(32), key + secret) + data).encode('base64') - return crypt.rstrip('\n') + return m.hexdigest() -def rc4(data, key): - """ - rc4 implementation - """ - x = 0 - box = range(256) - for i in range(256): - x = (x + box[i] + ord(key[i % len(key)])) % 256 - box[i], box[x] = box[x], box[i] - x = 0 - y = 0 - out = [] - for char in data: - x = (x + 1) % 256 - y = (y + box[x]) % 256 - box[x], box[y] = box[y], box[x] - out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) - - return ''.join(out) +def getb64(data): + return base64.b64encode(bytes(data, 'utf-8')).decode('utf-8') def getRandomLine(filename): @@ -337,7 +305,7 @@ def N_(message): return message def getCookie(self, value=""): try: - return urllib.unquote_plus(self._cookies[value].value) + return urllib.parse.unquote_plus(self._cookies[value].value) except KeyError: return None @@ -353,16 +321,16 @@ def setCookie(self, key, value="", max_age=None, expires=None, path="/", domain= """ if self._newcookies is None: self._newcookies = SimpleCookie() - self._newcookies[key] = urllib.quote_plus(value) + self._newcookies[key] = urllib.parse.quote_plus(value) if not max_age is None: self._newcookies[key]["max-age"] = max_age if not expires is None: - if isinstance(expires, basestring): + if isinstance(expires, str): self._newcookies[key]["expires"] = expires expires = None elif isinstance(expires, datetime): expires = expires.utctimetuple() - elif not isinstance(expires, (int, long)): + elif not isinstance(expires, int): expires = datetime.datetime.gmtime(expires) else: raise ValueError("Se requiere de un entero o un datetime") @@ -448,20 +416,20 @@ def inet_ntoa(packed_ip): def is_bad_proxy(pip): - import urllib2 + import urllib.request, urllib.error, urllib.parse import socket socket.setdefaulttimeout(3) try: - proxy_handler = urllib2.ProxyHandler({'http': pip}) - opener = urllib2.build_opener(proxy_handler) + proxy_handler = urllib.request.ProxyHandler({'http': pip}) + opener = urllib.request.build_opener(proxy_handler) opener.addheaders = [('User-agent', 'Mozilla/5.0')] - urllib2.install_opener(opener) - req = urllib2.Request('http://bienvenidoainternet.org') - sock = urllib2.urlopen(req) - except urllib2.HTTPError, e: + urllib.request.install_opener(opener) + req = urllib.request.Request('http://bienvenidoainternet.org') + sock = urllib.request.urlopen(req) + except urllib.error.HTTPError as e: return e.code - except Exception, detail: + except Exception as detail: return True return False diff --git a/cgi/img.py b/cgi/img.py index 1c57e37..7759b23 100644 --- a/cgi/img.py +++ b/cgi/img.py @@ -5,7 +5,7 @@ import math import os import subprocess import logging -from StringIO import StringIO +from io import BytesIO from settings import Settings from database import * @@ -35,7 +35,7 @@ def processImage(post, data, t, originalname, spoiler=False): # check the size is fine if size > int(board["maxsize"])*1024: - raise UserError, _("File too big. The maximum file size is: %s") % board['maxsize'] + raise UserError(_("File too big. The maximum file size is: %s") % board['maxsize']) # check if file is supported for filetype in board['filetypes']: @@ -44,12 +44,12 @@ def processImage(post, data, t, originalname, spoiler=False): break if not used_filetype: - raise UserError, _("File type not supported.") + raise UserError(_("File type not supported.")) # check if file is already posted is_duplicate = checkFileDuplicate(data) if checkFileDuplicate(data)[0]: - raise UserError, _("This image has already been posted %s.") % ('' + _("here") + '') + raise UserError(_("This image has already been posted %s.") % ('' + _("here") + '')) # prepare file names if used_filetype['preserve_name'] == '1': @@ -142,17 +142,17 @@ def processImage(post, data, t, originalname, spoiler=False): # generate thumbnails call_wrap(args) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: os.remove(file_path) logging.error("Thumbnail creation failure: " + e.output) - raise UserError, _("Thumbnail creation failure.") + ' ('+str(e.returncode)+')' + raise UserError(_("Thumbnail creation failure.") + ' ('+str(e.returncode)+')') # check if thumbnail was truly created try: open(file_thumb_path) except: os.remove(file_path) - raise UserError, _("Thumbnail creation failure.") + raise UserError(_("Thumbnail creation failure.")) # create extra thumbnails (catalog/mobile) subprocess.call([Settings.CONVERT_PATH, file_thumb_path, "-limit", "thread", @@ -182,7 +182,7 @@ def processImage(post, data, t, originalname, spoiler=False): post["message"] += extraInfo(content_type, file_name, file_path) # file md5 - post["file_hex"] = getMD5(data) + post["file_hex"] = getMD5b(data) return post @@ -233,7 +233,6 @@ def extraInfo(mime, file_name, file_path): def getImageInfo(data): - data = str(data) size = len(data) height = -1 width = -1 @@ -251,7 +250,7 @@ def getImageInfo(data): # See PNG 2. Edition spec (http://www.w3.org/TR/PNG/) # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR' # and finally the 4-byte width, height - elif ((size >= 24) and data.startswith("\211PNG\r\n\032\n") + elif ((size >= 24) and data.startswith(b"\211PNG\r\n\032\n") and (data[12:16] == "IHDR")): content_type = "image/png" w, h = struct.unpack(">LL", data[16:24]) @@ -259,7 +258,7 @@ def getImageInfo(data): height = int(h) # Maybe this is for an older PNG version. - elif (size >= 16) and data.startswith("\211PNG\r\n\032\n"): + elif (size >= 16) and data.startswith(b"\211PNG\r\n\032\n"): # Check to see if we have the right content type content_type = "image/png" w, h = struct.unpack(">LL", data[8:16]) @@ -267,9 +266,9 @@ def getImageInfo(data): height = int(h) # handle JPEGs - elif (size >= 2) and data.startswith("\377\330"): + elif (size >= 2) and data.startswith(b"\377\330"): content_type = "image/jpeg" - jpeg = StringIO(data) + jpeg = BytesIO(data) jpeg.read(2) b = jpeg.read(1) try: @@ -288,9 +287,9 @@ def getImageInfo(data): width = int(w) height = int(h) except struct.error: - pass + raise except ValueError: - pass + raise # handle WebP if data[:4] == b'RIFF' and data[8:12] == b'WEBP': @@ -309,7 +308,7 @@ def getImageInfo(data): content_type = "image/webp" # handle WebM - elif (size >= 4) and data.startswith("\x1A\x45\xDF\xA3"): + elif (size >= 4) and data.startswith(b"\x1A\x45\xDF\xA3"): content_type = "video/webm" info = ffprobe(data) @@ -333,7 +332,7 @@ def getImageInfo(data): content_type = "audio/mod" # handle XM - elif (size >= 64) and data.startswith("Extended Module:"): + elif (size >= 64) and data.startswith(b"Extended Module:"): content_type = "audio/xm" # handle S3M @@ -418,11 +417,10 @@ def checkFileDuplicate(data): """ board = Settings._.BOARD - file_hex = getMD5(data) - post = FetchOne("SELECT `id`, `parentid` FROM `posts` WHERE `file_hex` = '%s' AND `boardid` = %s AND IS_DELETED = 0 LIMIT 1" % ( - file_hex, board['id'])) + file_hex = getMD5b(data) + post = FetchOne("SELECT `id`, `parentid` FROM `posts` WHERE `file_hex` = %s AND `boardid` = %s AND IS_DELETED = 0 LIMIT 1", (file_hex, board['id'])) if post: - if int(post["parentid"]) != 0: + if post["parentid"]: return True, post["parentid"], post["id"] else: return True, post["id"], post["id"] diff --git a/cgi/manage.py b/cgi/manage.py index 0ad2d48..40be3b2 100644 --- a/cgi/manage.py +++ b/cgi/manage.py @@ -1,7 +1,7 @@ # coding=utf-8 -import _mysql import os import cgi +import html import shutil import datetime import logging @@ -24,18 +24,15 @@ def manage(self, path_split): if 'username' in self.formdata and 'password' in self.formdata: # If no admin accounts available, create admin:admin - first_admin = FetchOne("SELECT 1 FROM `staff` WHERE `rights` = 0 LIMIT 1", 0) + first_admin = FetchOne("SELECT 1 FROM `staff` WHERE `rights` = 0 LIMIT 1") if not first_admin: - InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES ('admin', '" + - _mysql.escape_string(genPasswdHash("admin")) + "', 0, 0)") + InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES ('admin', %s, 0, 0)", (genPasswdHash("admin"),)) - staff_account = verifyPasswd( - self.formdata['username'], self.formdata['password']) + staff_account = verifyPasswd(self.formdata['username'], self.formdata['password']) if staff_account: session_uuid = newSession(staff_account['id']) setCookie(self, 'weabot_manage', session_uuid) - UpdateDb('DELETE FROM `logs` WHERE `timestamp` < ' + - str(timestamp() - Settings.MANAGE_LOG_TIME)) # three months + UpdateDb("DELETE FROM `logs` WHERE `timestamp` < %s", (timestamp() - Settings.MANAGE_LOG_TIME,)) else: page += _('Incorrect username/password.') logAction('', 'Failed log-in. U:'+_mysql.escape_string(self.formdata['username'])+' IP logged.') @@ -54,12 +51,12 @@ def manage(self, path_split): if 'session_id' in staff_account: renewSession(staff_account['session_id']) - if staff_account['rights'] in ['0', '1', '2']: + if staff_account['rights'] in [0, 1, 2]: administrator = True - if staff_account['rights'] == '2': + if staff_account['rights'] == 2: moderator = False - UpdateDb('UPDATE `staff` SET `lastactive` = ' + str(timestamp() - ) + ' WHERE `id` = ' + staff_account['id'] + ' LIMIT 1') + UpdateDb('UPDATE `staff` SET `lastactive` = %s WHERE `id` = %s LIMIT 1', + (timestamp(), staff_account['id'])) if not validated: template_filename = "login.html" @@ -178,11 +175,11 @@ def manage(self, path_split): elif len(path_split) > 4: parentid = int(path_split[4]) # make sure it's the full thread - check = FetchOne("SELECT `parentid` FROM `posts` WHERE `id` = %s AND `boardid` = %s LIMIT 1" % (parentid, board['id'])) - if check['parentid'] != "0": + check = FetchOne("SELECT `parentid` FROM `posts` WHERE `id` = %s AND `boardid` = %s LIMIT 1", (parentid, board['id'])) + if check['parentid']: parentid = int(check['parentid']) - posts = FetchAll('SELECT id, timestamp, timestamp_formatted, name, message, file, thumb, IS_DELETED, locked, subject, length, INET6_NTOA(ip) AS ip FROM `posts` WHERE (parentid = %d OR id = %d) AND boardid = %s ORDER BY `id` ASC' % (parentid, parentid, board['id'])) + posts = FetchAll('SELECT id, timestamp, timestamp_formatted, name, message, file, thumb, IS_DELETED, locked, subject, length, INET6_NTOA(ip) AS ip FROM `posts` WHERE (parentid = %s OR id = %s) AND boardid = %s ORDER BY `id` ASC', (parentid, parentid, board['id'])) template_filename = "mod.html" template_values = {"mode": 3, "dir": board["dir"], "posts": posts} else: @@ -190,11 +187,11 @@ def manage(self, path_split): template_filename = "mod.html" template_values = {"mode": 2, "dir": board["dir"], "threads": threads} elif path_split[2] == "recent": - posts = FetchAll("SELECT posts.id, posts.subject, dir, boards.board_type, parentid, file, thumb, timestamp_formatted, timestamp, posts.message, INET6_NTOA(ip) AS ip, posts.name, email, tripcode, boards.name AS board_name FROM posts INNER JOIN boards ON posts.boardid = boards.id WHERE posts.timestamp > UNIX_TIMESTAMP() - 86400 ORDER BY timestamp DESC") + posts = FetchAll("SELECT posts.id, posts.subject, dir, boards.board_type, CASE parentid WHEN '0' THEN posts.id ELSE parentid END AS parentid, file, thumb, timestamp_formatted, timestamp, posts.message, INET6_NTOA(ip) AS ip, posts.name, email, tripcode, boards.name AS board_name FROM posts INNER JOIN boards ON posts.boardid = boards.id WHERE posts.timestamp > UNIX_TIMESTAMP() - 86400 ORDER BY timestamp DESC") template_filename = "recent.html" template_values = {"posts": posts} elif path_split[2] == 'staff': - if staff_account['rights'] != '0': + if staff_account['rights'] != 0: return action_taken = False @@ -202,23 +199,23 @@ def manage(self, path_split): if path_split[3] == 'add' or path_split[3] == 'edit': member = None member_username = '' - member_rights = '3' + member_rights = 3 if path_split[3] == 'edit': if len(path_split) > 4: - member = FetchOne('SELECT * FROM `staff` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1') + member = FetchOne('SELECT * FROM `staff` WHERE `id` = %s LIMIT 1', (path_split[4],)) if member: member_username = member['username'] member_rights = member['rights'] - action = 'edit/' + member['id'] + action = 'edit/' + str(member['id']) try: if self.formdata.get('user'): - if self.formdata['rights'] in ['0', '1', '2', '3']: + if self.formdata['rights'] in [0, 1, 2, 3]: action_taken = True - UpdateDb("UPDATE `staff` SET `username` = '" + _mysql.escape_string( - self.formdata['user']) + "', `rights` = " + self.formdata['rights'] + " WHERE `id` = " + member['id'] + " LIMIT 1") + UpdateDb("UPDATE `staff` SET `username` = %s, `rights` = %s WHERE `id` = LIMIT 1", + (self.formdata['user'], self.formdata['rights'], member['id'])) message = _( 'Staff member updated.') logAction(staff_account['username'], _( @@ -231,15 +228,15 @@ def manage(self, path_split): try: if self.formdata.get('user') and self.formdata.get('pass'): username_taken = FetchOne( - 'SELECT * FROM `staff` WHERE `username` = \'' + _mysql.escape_string(self.formdata['user']) + '\' LIMIT 1') + 'SELECT * FROM `staff` WHERE `username` = %s LIMIT 1', (self.formdata['user'],)) if not username_taken: - if self.formdata['rights'] in ['0', '1', '2', '3']: + if self.formdata['rights'] in [0, 1, 2, 3]: action_taken = True pass_hash = genPasswdHash( self.formdata['pass']) - InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES ('" + _mysql.escape_string( - self.formdata['user']) + "', '" + _mysql.escape_string(pass_hash) + "', " + str(timestamp()) + ", " + self.formdata['rights'] + ")") + InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES (%s, %s, %s, %s)", + (self.formdata['user'], pass_hash, timestamp(), self.formdata['rights'])) message = _('Staff member added.') logAction( staff_account['username'], 'Added staff account for ' + self.formdata['user']) @@ -301,21 +298,21 @@ def manage(self, path_split): if not action_taken: staff = FetchAll('SELECT * FROM `staff` ORDER BY `rights`') for member in staff: - if member['rights'] == '0': + if member['rights'] == 0: member['rights'] = _('Super-administrator') - elif member['rights'] == '1': + elif member['rights'] == 1: member['rights'] = _('Administrator') - elif member['rights'] == '2': + elif member['rights'] == 2: member['rights'] = _('Developer') - elif member['rights'] == '3': + elif member['rights'] == 3: member['rights'] = _('Moderator') - if member['lastactive'] != '0': + if member['lastactive']: member['lastactivestamp'] = member['lastactive'] member['lastactive'] = formatTimestamp( member['lastactive']) else: member['lastactive'] = _('Never') - member['lastactivestamp'] = '0' + member['lastactivestamp'] = 0 template_filename = "staff.html" template_values = {'mode': 0, 'staff': staff} elif path_split[2] == 'delete': @@ -562,7 +559,7 @@ def manage(self, path_split): num += 1 # fix anchors - for old, new in refs.iteritems(): + for old, new in refs.items(): old_url = "/{oldboard}/res/{oldthread}.html#{oldpost}\">>>{oldpost}".format( oldboard=oldboard, oldthread=oldthread, oldpost=old) @@ -685,7 +682,7 @@ def manage(self, path_split): else: self.error("IP o rango inválido.") return - except netaddr.core.AddrFormatError, e: + except netaddr.core.AddrFormatError as e: self.error("Problema con el IP o rango ingresado: {}".format(e)) return @@ -695,12 +692,12 @@ def manage(self, path_split): else: until = '0' where = '' - if 'board_all' not in self.formdata.keys(): + if 'board_all' not in self.formdata: where = [] boards = FetchAll('SELECT `dir` FROM `boards`') for board in boards: keyname = 'board_' + board['dir'] - if keyname in self.formdata.keys(): + if keyname in self.formdata: if self.formdata[keyname] == "1": where.append(board['dir']) if len(where) > 0: @@ -710,7 +707,7 @@ def manage(self, path_split): _("You must select where the ban shall be placed")) return - if 'edit' in self.formdata.keys(): + if 'edit' in self.formdata: UpdateDb("DELETE FROM `bans` WHERE `id` = '" + _mysql.escape_string(self.formdata['edit']) + "' LIMIT 1") """else: # TODO : Duplicate check @@ -732,7 +729,7 @@ def manage(self, path_split): _mysql.escape_string(where) + "', " + str(timestamp()) + ", " + until + ", '" + _mysql.escape_string(staff_account['username']) + "', '" + _mysql.escape_string(self.formdata['reason']) + "', '" + _mysql.escape_string(self.formdata['note']) + "', '"+blind+"')") regenerateAccess() - if 'edit' in self.formdata.keys(): + if 'edit' in self.formdata: message = _('Ban successfully edited.') action = 'Edited ban for ' + ip else: @@ -753,7 +750,7 @@ def manage(self, path_split): 'seconds': '0', 'blind': '1'} edit_id = 0 - if 'edit' in self.formdata.keys(): + if 'edit' in self.formdata: edit_id = self.formdata['edit'] ban = FetchOne("SELECT `id`, INET6_NTOA(`ip`) AS 'ip', CASE WHEN `netmask` IS NULL THEN '255.255.255.255' ELSE INET_NTOA(`netmask`) END AS 'netmask', boards, added, until, staff, reason, note, blind FROM `bans` WHERE `id` = '" + _mysql.escape_string(edit_id) + "' ORDER BY `added` DESC") @@ -812,7 +809,7 @@ def manage(self, path_split): if ban['boards'] == '': ban['boards'] = _('All boards') else: - where = pickle.loads(ban['boards']) + where = pickle.loads(ban['boards'].encode('utf-8')) if len(where) > 1: ban['boards'] = '/' + \ '/, /'.join(where) + '/' @@ -878,48 +875,48 @@ def manage(self, path_split): board['useid'] = self.formdata['useid'] board['slip'] = self.formdata['slip'] board['countrycode'] = self.formdata['countrycode'] - if 'recyclebin' in self.formdata.keys(): + if 'recyclebin' in self.formdata: board['recyclebin'] = '1' else: board['recyclebin'] = '0' - if 'disable_name' in self.formdata.keys(): + if 'disable_name' in self.formdata: board['disable_name'] = '1' else: board['disable_name'] = '0' - if 'disable_subject' in self.formdata.keys(): + if 'disable_subject' in self.formdata: board['disable_subject'] = '1' else: board['disable_subject'] = '0' - if 'secret' in self.formdata.keys(): + if 'secret' in self.formdata: board['secret'] = '1' else: board['secret'] = '0' - if 'locked' in self.formdata.keys(): + if 'locked' in self.formdata: board['locked'] = '1' else: board['locked'] = '0' board['postarea_desc'] = self.formdata['postarea_desc'] - if 'allow_noimage' in self.formdata.keys(): + if 'allow_noimage' in self.formdata: board['allow_noimage'] = '1' else: board['allow_noimage'] = '0' - if 'allow_images' in self.formdata.keys(): + if 'allow_images' in self.formdata: board['allow_images'] = '1' else: board['allow_images'] = '0' - if 'allow_image_replies' in self.formdata.keys(): + if 'allow_image_replies' in self.formdata: board['allow_image_replies'] = '1' else: board['allow_image_replies'] = '0' - if 'allow_spoilers' in self.formdata.keys(): + if 'allow_spoilers' in self.formdata: board['allow_spoilers'] = '1' else: board['allow_spoilers'] = '0' - if 'allow_oekaki' in self.formdata.keys(): + if 'allow_oekaki' in self.formdata: board['allow_oekaki'] = '1' else: board['allow_oekaki'] = '0' - if 'archive' in self.formdata.keys(): + if 'archive' in self.formdata: board['archive'] = '1' else: board['archive'] = '0' @@ -930,7 +927,7 @@ def manage(self, path_split): UpdateDb( "DELETE FROM `boards_filetypes` WHERE `boardid` = %s" % board['id']) for filetype in filetypelist(): - if 'filetype'+filetype['ext'] in self.formdata.keys(): + if 'filetype'+filetype['ext'] in self.formdata: UpdateDb("INSERT INTO `boards_filetypes` VALUES (%s, %s)" % ( board['id'], filetype['id'])) @@ -938,49 +935,49 @@ def manage(self, path_split): board['numthreads'] = int( self.formdata['numthreads']) except: - raise UserError, _("Max threads shown must be numeric.") + raise UserError(_("Max threads shown must be numeric.")) try: board['numcont'] = int(self.formdata['numcont']) except: - raise UserError, _("Max replies shown must be numeric.") + raise UserError(_("Max replies shown must be numeric.")) try: board['numline'] = int(self.formdata['numline']) except: - raise UserError, _("Max lines shown must be numeric.") + raise UserError(_("Max lines shown must be numeric.")) try: board['thumb_px'] = int(self.formdata['thumb_px']) except: - raise UserError, _("Max thumb dimensions must be numeric.") + raise UserError(_("Max thumb dimensions must be numeric.")) try: board['maxsize'] = int(self.formdata['maxsize']) except: - raise UserError, _("Max size must be numeric.") + raise UserError(_("Max size must be numeric.")) try: board['maxage'] = int(self.formdata['maxage']) except: - raise UserError, _("Max age must be numeric.") + raise UserError(_("Max age must be numeric.")) try: board['maxinactive'] = int( self.formdata['maxinactive']) except: - raise UserError, _("Max inactivity must be numeric.") + raise UserError(_("Max inactivity must be numeric.")) try: board['threadsecs'] = int( self.formdata['threadsecs']) except: - raise UserError, _("Time between new threads must be numeric.") + raise UserError(_("Time between new threads must be numeric.")) try: board['postsecs'] = int(self.formdata['postsecs']) except: - raise UserError, _("Time between replies must be numeric.") + raise UserError(_("Time between replies must be numeric.")) updateBoardSettings() message = _('Board options successfully updated.') + ' 3 and path_split[3] == 'add': - if "add" in self.formdata.keys(): + if "add" in self.formdata: edit_id = 0 - if 'edit' in self.formdata.keys(): + if 'edit' in self.formdata: edit_id = int(self.formdata['edit']) # We decide what type of filter it is. @@ -1447,12 +1444,12 @@ def manage(self, path_split): # I don't like pickles... oh well. where = '' - if 'board_all' not in self.formdata.keys(): + if 'board_all' not in self.formdata: where = [] boards = FetchAll('SELECT `dir` FROM `boards`') for board in boards: keyname = 'board_' + board['dir'] - if keyname in self.formdata.keys(): + if keyname in self.formdata: if self.formdata[keyname] == "1": where.append(board['dir']) if len(where) > 0: @@ -1466,8 +1463,7 @@ def manage(self, path_split): if filter_type == 0: # Word filter if len(self.formdata["word"]) > 0: - filter_from = _mysql.escape_string( - cgi.escape(self.formdata["word"])) + filter_from = html.escape(self.formdata["word"]) else: self.error(_("You must enter a word.")) return @@ -1475,12 +1471,10 @@ def manage(self, path_split): # Name/trip filter can_add = False if len(self.formdata["name"]) > 0: - filter_from = _mysql.escape_string( - self.formdata["name"]) + filter_from = self.formdata["name"] can_add = True if len(self.formdata["trip"]) > 0: - filter_tripcode = _mysql.escape_string( - self.formdata["trip"]) + filter_tripcode = self.formdata["trip"] can_add = True if not can_add: self.error( @@ -1491,21 +1485,18 @@ def manage(self, path_split): sql_query = '' filter_reason = '' if len(self.formdata["reason"]) > 0: - filter_reason = _mysql.escape_string( - self.formdata["reason"]) + filter_reason = self.formdata["reason"] if filter_action == 0: # Cancel post - sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \ - (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, str( - timestamp()), _mysql.escape_string(staff_account['username'])) + sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `added`, `staff`) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)" + sql_params = (edit_id, where, filter_type, filter_action, filter_from, filter_tripcode, filter_reason, timestamp(), staff_account['username']) elif filter_action == 1: # Change to if len(self.formdata["changeto"]) > 0: filter_to = _mysql.escape_string( self.formdata["changeto"]) - sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `to`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \ - (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, filter_to, str( - timestamp()), _mysql.escape_string(staff_account['username'])) + sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `to`, `added`, `staff`) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" + sql_params = (edit_id, where, filter_type, filter_action, filter_from, filter_tripcode, filter_reason, filter_to, timestamp(), staff_account['username']) else: self.error( _("You must enter a word to change to.")) @@ -1516,7 +1507,7 @@ def manage(self, path_split): if len(self.formdata["seconds"]) > 0: filter_seconds = _mysql.escape_string( self.formdata["seconds"]) - if "blind" in self.formdata.keys() and self.formdata["blind"] == '1': + if "blind" in self.formdata and self.formdata["blind"] == '1': filter_blind = '1' else: filter_blind = '2' @@ -1564,7 +1555,7 @@ def manage(self, path_split): else: # Create add form edit_id = 0 - if 'edit' in self.formdata.keys() and int(self.formdata['edit']) > 0: + if 'edit' in self.formdata and int(self.formdata['edit']) > 0: # Load values edit_id = int(self.formdata['edit']) filt = FetchOne( @@ -1627,21 +1618,20 @@ def manage(self, path_split): filters = FetchAll( "SELECT * FROM `filters` ORDER BY `added` DESC") for filter in filters: - if filter['boards'] == '': + if not filter['boards']: filter['boards'] = _('All boards') else: - where = pickle.loads(filter['boards']) + where = pickle.loads(filter['boards'].encode('utf-8')) if len(where) > 1: filter['boards'] = '/' + \ '/, /'.join(where) + '/' else: filter['boards'] = '/' + where[0] + '/' - if filter['type'] == '0': - filter['type_formatted'] = _( - 'Word:') + ' ' + cgi.escape(filter['from']) + '' - elif filter['type'] == '1': + if filter['type'] == 0: + filter['type_formatted'] = _('Word:') + ' ' + html.escape(filter['from']) + '' + elif filter['type'] == 1: filter['type_formatted'] = _('Name/Tripcode:')+' ' - if filter['from'] != '': + if filter['from']: filter['type_formatted'] += '' + \ filter['from'] + '' if filter['from_trip'] != '': @@ -1649,20 +1639,20 @@ def manage(self, path_split): filter['from_trip'] + '' else: filter['type_formatted'] = '?' - if filter['action'] == '0': + if filter['action'] == 0: filter['action_formatted'] = _('Abort post') - elif filter['action'] == '1': + elif filter['action'] == 1: filter['action_formatted'] = _( - 'Change to:') + ' ' + cgi.escape(filter['to']) + '' - elif filter['action'] == '2': - if filter['blind'] == '1': + 'Change to:') + ' ' + html.escape(filter['to']) + '' + elif filter['action'] == 2: + if filter['blind'] == 1: blind = _('Yes') else: blind = _('No') filter['action_formatted'] = _('Autoban:') + '
' + \ (_('Length:')+' %s
'+_('Blind:') + ' %s') % (filter['seconds'], blind) - elif filter['action'] == '3': + elif filter['action'] == 3: filter['action_formatted'] = (_('Redirect to:')+' %s ('+_('in %s secs')+')') % ( filter['redirect_url'], filter['redirect_time']) else: @@ -1672,7 +1662,7 @@ def manage(self, path_split): template_filename = "filters.html" template_values = {'mode': 0, 'filters': filters} elif path_split[2] == 'logs': - if staff_account['rights'] != '0' and staff_account['rights'] != '2': + if staff_account['rights'] not in [0, 2]: return logs = FetchAll( @@ -1690,7 +1680,7 @@ def manage(self, path_split): template_filename = "message.html" elif path_split[2] == 'quotes': # Quotes for the post screen - if "save" in self.formdata.keys(): + if "save" in self.formdata: try: f = open('quotes.conf', 'w') f.write(self.formdata["data"]) @@ -1731,7 +1721,7 @@ def manage(self, path_split): type = int(self.formdata['type']) if type > 2: - raise UserError, "Tipo no soportado" + raise UserError("Tipo no soportado") # canal del home if len(path_split) > 3: @@ -1748,7 +1738,7 @@ def manage(self, path_split): title = self.formdata["title"] # Post anonimo - if 'anonymous' in self.formdata.keys() and self.formdata['anonymous'] == '1': + if 'anonymous' in self.formdata and self.formdata['anonymous'] == '1': to_name = "Staff ★" else: to_name = "%s ★" % staff_account['username'] @@ -1758,7 +1748,8 @@ def manage(self, path_split): else: timestamp_formatted = re.sub(r"\(...\)", " ", timestamp_formatted) - UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%d', '%s')" % (type, staff_account['id'], staff_account['username'], _mysql.escape_string(title), _mysql.escape_string(message), to_name, timestamp(t), timestamp_formatted)) + UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)", + (type, staff_account['id'], staff_account['username'], title, message, to_name, timestamp(t), timestamp_formatted)) regenerateNews() regenerateHome() @@ -1804,14 +1795,14 @@ def manage(self, path_split): pass # If it's preferred to remain anonymous... - if 'anonymous' in self.formdata.keys() and self.formdata['anonymous'] == '1': + if 'anonymous' in self.formdata and self.formdata['anonymous'] == '1': to_name = "Staff ★" else: to_name = "%s ★" % staff_account['username'] timestamp_formatted = formatDate(t) - UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES ('0', '%s', '%s', '%s', '%s', '%s', '%d', '%s')" % ( - staff_account['id'], staff_account['username'], _mysql.escape_string(self.formdata['title']), _mysql.escape_string(message), to_name, timestamp(t), timestamp_formatted)) + UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES (0, %s, %s, %s, %s, %s, %s, %s)", + (staff_account['id'], staff_account['username'], self.formdata['title'], message, to_name, timestamp(t), timestamp_formatted)) message = _("Added successfully.") template_filename = "message.html" @@ -1832,7 +1823,8 @@ def manage(self, path_split): if administrator: posts = FetchAll("SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC") else: - posts = FetchAll("SELECT * FROM `news` WHERE staffid = '" + staff_account['id']+"' AND type = '0' ORDER BY `timestamp` DESC") + posts = FetchAll("SELECT * FROM `news` WHERE staffid = %s AND type = 0 ORDER BY `timestamp` DESC", + (staff_account['id'],)) template_filename = "news.html" template_values = {'action': 'newschannel', 'posts': posts} @@ -1858,9 +1850,9 @@ def manage(self, path_split): UpdateDb("DELETE FROM `reports` WHERE `id` = '" + _mysql.escape_string(path_split[5])+"'") message = _('Report %s ignored.') % path_split[5] - if 'ignore' in self.formdata.keys(): + if 'ignore' in self.formdata: ignored = 0 - if 'board' in self.formdata.keys() and self.formdata['board'] != 'all': + if 'board' in self.formdata and self.formdata['board'] != 'all': reports = FetchAll("SELECT `id` FROM `reports` WHERE `board` = '%s' ORDER BY `timestamp` DESC LIMIT %d, %d" % ( _mysql.escape_string(self.formdata['board']), currentpage*pagesize, pagesize)) else: @@ -1869,7 +1861,7 @@ def manage(self, path_split): for report in reports: keyname = 'i' + report['id'] - if keyname in self.formdata.keys(): + if keyname in self.formdata: # Ignore here UpdateDb("DELETE FROM `reports` WHERE `id` = '" + _mysql.escape_string(report['id'])+"'") @@ -1881,20 +1873,20 @@ def manage(self, path_split): boards = FetchAll( 'SELECT `name`, `dir` FROM `boards` ORDER BY `dir`') for board in boards: - if 'board' in self.formdata.keys() and self.formdata['board'] == board['dir']: + if 'board' in self.formdata and self.formdata['board'] == board['dir']: board['checked'] = True else: board['checked'] = False # Tabla - if 'board' in self.formdata.keys() and self.formdata['board'] != 'all': + if 'board' in self.formdata and self.formdata['board'] != 'all': reports = FetchAll("SELECT id, timestamp, timestamp_formatted, postid, parentid, link, board, INET6_NTOA(ip) AS ip, reason, INET6_NTOA(repip) AS repip FROM `reports` WHERE `board` = '%s' ORDER BY `timestamp` DESC LIMIT %d, %d" % ( _mysql.escape_string(self.formdata['board']), currentpage*pagesize, pagesize)) else: reports = FetchAll("SELECT id, timestamp, timestamp_formatted, postid, parentid, link, board, INET6_NTOA(ip) AS ip, reason, INET6_NTOA(repip) AS repip FROM `reports` ORDER BY `timestamp` DESC LIMIT %d, %d" % ( currentpage*pagesize, pagesize)) - if 'board' in self.formdata.keys(): + if 'board' in self.formdata: curboard = self.formdata['board'] else: curboard = None @@ -1937,11 +1929,11 @@ def manage(self, path_split): if not moderator: return - if 'ip' in self.formdata.keys(): + if 'ip' in self.formdata: # If an IP was given... if self.formdata['ip'] != '': ip = self.formdata['ip'] - posts = FetchAll("SELECT posts.*, boards.dir, boards.board_type, boards.subject AS default_subject FROM `posts` JOIN `boards` ON boards.id = posts.boardid WHERE ip = INET6_ATON('%s') ORDER BY posts.timestamp DESC" % _mysql.escape_string(ip)) + posts = FetchAll("SELECT posts.*, boards.dir, boards.board_type, boards.subject AS default_subject FROM `posts` JOIN `boards` ON boards.id = posts.boardid WHERE ip = INET6_ATON(%s) ORDER BY posts.timestamp DESC", (ip,)) template_filename = "ipshow.html" template_values = {"mode": 1, "ip": ip, "host": getHost( ip), "country": getCountry(ip), "tor": addressIsTor(ip), "posts": posts} @@ -1956,17 +1948,17 @@ def manage(self, path_split): return # Delete by IP - if 'ip' in self.formdata.keys(): + if 'ip' in self.formdata: # If an IP was given... if self.formdata['ip'] != '': where = [] - if 'board_all' not in self.formdata.keys(): + if 'board_all' not in self.formdata: # If he chose boards separately, add them to a list boards = FetchAll( 'SELECT `id`, `dir` FROM `boards`') for board in boards: keyname = 'board_' + board['dir'] - if keyname in self.formdata.keys(): + if keyname in self.formdata: if self.formdata[keyname] == "1": where.append(board) else: @@ -2032,7 +2024,7 @@ def manage(self, path_split): try: pid = int(path_split[4]) except ValueError: - raise UserError, "ID no válida." + raise UserError("ID no válida.") if board_type == '1': first = get_parent_post(pid, board['id']) @@ -2094,8 +2086,8 @@ def manage(self, path_split): template_values = {'search': search_logs} else: # Main page. - reports = FetchOne("SELECT COUNT(1) FROM `reports`", 0)[0] - posts = FetchAll("SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC") + reports = FetchOne("SELECT COUNT(1) AS 'count' FROM `reports`")["count"] + posts = FetchAll("SELECT * FROM `news` WHERE type = 0 ORDER BY `timestamp` DESC") template_filename = "manage.html" template_values = {'reports': reports, 'posts': posts} @@ -2134,12 +2126,12 @@ def switchBoard(new_type): kako_dir = os.path.join(Settings.ROOT_DIR, board['dir'], 'kako') res_dir = os.path.join(Settings.ROOT_DIR, board['dir'], 'res') - if new_type == '0': + if new_type == 0: # Switching to Imageboard # Delete kako if empty if os.path.exists(kako_dir) and not os.listdir(kako_dir): os.rmdir(kako_dir) - elif new_type == '1': + elif new_type == 1: # Switching to Textboard # Make kako dir if not os.path.exists(kako_dir): @@ -2152,13 +2144,10 @@ def switchBoard(new_type): def newSession(staff_id): import uuid session_uuid = uuid.uuid4().hex + expires = timestamp() + Settings.SESSION_TIME - param_session_id = _mysql.escape_string(session_uuid) - param_expires = timestamp() + Settings.SESSION_TIME - param_staff_id = int(staff_id) - - InsertDb("INSERT INTO `session` (`session_id`, `expires`, `staff_id`) VALUES (UNHEX('%s'), %d, %d)" % - (param_session_id, param_expires, param_staff_id)) + InsertDb("INSERT INTO `session` (`session_id`, `expires`, `staff_id`) VALUES (UNHEX(%s), %s, %s)", + (session_uuid, expires, staff_id)) return session_uuid @@ -2166,13 +2155,11 @@ def newSession(staff_id): def validateSession(session_id): cleanSessions() - param_session_id = _mysql.escape_string(session_id) - param_now = timestamp() session = FetchOne( "SELECT HEX(session_id) as session_id, id, username, rights, added FROM `session` " "INNER JOIN `staff` ON `session`.`staff_id` = `staff`.`id` " - "WHERE `session_id` = UNHEX('%s')" % - (param_session_id)) + "WHERE `session_id` = UNHEX(%s)", + (session_id,)) if session: return session @@ -2181,29 +2168,22 @@ def validateSession(session_id): def renewSession(session_id): - param_session_id = _mysql.escape_string(session_id) - param_expires = timestamp() + Settings.SESSION_TIME + expires = timestamp() + Settings.SESSION_TIME - UpdateDb("UPDATE `session` SET expires = %d WHERE session_id = UNHEX('%s')" % - (param_expires, param_session_id)) + UpdateDb("UPDATE `session` SET expires = %s WHERE session_id = UNHEX(%s)", (expires, session_id)) def deleteSession(session_id): - param_session_id = _mysql.escape_string(session_id) - - UpdateDb("DELETE FROM `session` WHERE session_id = UNHEX('%s')" % - param_session_id) + UpdateDb("DELETE FROM `session` WHERE session_id = UNHEX(%s)", (param_session_id,)) def cleanSessions(): - param_now = timestamp() - - UpdateDb("DELETE FROM `session` WHERE expires <= %d" % param_now) + UpdateDb("DELETE FROM `session` WHERE expires <= %s", (timestamp(),)) def logAction(staff, action): - InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + str(timestamp()) + - ", '" + _mysql.escape_string(staff) + "\', \'" + _mysql.escape_string(action) + "\')") + InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (%s, %s, %s)", + (timestamp(), staff, action)) def genPasswdHash(string): @@ -2217,9 +2197,8 @@ def verifyPasswd(username, passwd): import argon2 ph = argon2.PasswordHasher() - param_username = _mysql.escape_string(username) staff_account = FetchOne( - "SELECT * FROM staff WHERE username = '%s'" % param_username) + "SELECT * FROM staff WHERE username = %s", (username,)) if not staff_account: return None @@ -2228,7 +2207,7 @@ def verifyPasswd(username, passwd): except argon2.exceptions.VerifyMismatchError: return None except argon2.exceptions.InvalidHash: - raise UserError, "Hash obsoleto o inválido. Por favor contacte al administrador." + raise UserError("Hash obsoleto o inválido. Por favor contacte al administrador.") if ph.check_needs_rehash(staff_account['password']): param_new_hash = ph.hash(staff_acount['password']) diff --git a/cgi/modapi.py b/cgi/modapi.py index 7bb63fb..499535e 100644 --- a/cgi/modapi.py +++ b/cgi/modapi.py @@ -12,11 +12,11 @@ def api(self, path_split): if len(path_split) > 2: try: self.output = api_process(self, path_split) - except APIError, e: + except APIError as e: self.output = api_error("error", e.message) - except UserError, e: + except UserError as e: self.output = api_error("failed", e.message) - except Exception, e: + except Exception as e: import sys import traceback exc_type, exc_value, exc_traceback = sys.exc_info() @@ -65,11 +65,11 @@ def api_process(self, path_split): else: logAction('', 'Failed log-in. Username:'+_mysql.escape_string( self.formdata['username'])+' IP:'+self.environ["REMOTE_ADDR"]) - raise APIError, "Incorrect username/password." + raise APIError("Incorrect username/password.") else: - raise APIError, "Bad request" + raise APIError("Bad request") else: - raise APIError, "Not authenticated" + raise APIError("Not authenticated") else: if method == 'news': news = FetchAll( @@ -77,7 +77,7 @@ def api_process(self, path_split): values['news'] = news elif method == 'post': board = setBoard(formdata.get("board")) - if 'id' in formdata.keys(): + if 'id' in formdata: id = formdata.get('id') post = FetchOne("SELECT `id`, `boardid`, `parentid`,`timestamp`, `name`, `tripcode`, `email` ,`subject`,`message`,`file`,`thumb`, INET6_NTOA(`ip`) as ip,`IS_DELETED` AS `deleted`, `bumped`, `last`, `locked` FROM `posts` WHERE `id` = '" + _mysql.escape_string(id) + "' AND `boardid` = '" + _mysql.escape_string(board["id"]) + "'") @@ -90,7 +90,7 @@ def api_process(self, path_split): post['timestamp'] = int(post['timestamp']) post['boardid'] = int(post['boardid']) values['post'] = post - if 'parentid' in formdata.keys(): + if 'parentid' in formdata: id = formdata.get('parentid') posts = FetchAll("SELECT `id`, `boardid`, `parentid`,`timestamp`, `name`, `tripcode`, `email` ,`subject`,`message`,`file`,`thumb`, INET6_NTOA(`ip`) as ip,`IS_DELETED` AS `deleted`, `bumped`, `last`, `locked` FROM `posts` WHERE `parentid` = '" + _mysql.escape_string(id) + "' AND `boardid` = '" + _mysql.escape_string(board["id"]) + "'") @@ -114,28 +114,28 @@ def api_process(self, path_split): numreplies = 2 if not data_board: - raise APIError, "Missing parameters" + raise APIError("Missing parameters") if data_limit: try: limit = int(data_limit) except ValueError: - raise APIError, "Limit must be numeric" + raise APIError("Limit must be numeric") if data_offset: try: offset = int(data_offset) except ValueError: - raise APIError, "Offset must be numeric" + raise APIError("Offset must be numeric") if data_replies: try: numreplies = int(data_replies) except ValueError: - raise APIError, "Replies must be numeric" + raise APIError("Replies must be numeric") if data_replies and limit > 30: - raise APIError, "Maximum limit is 30" + raise APIError("Maximum limit is 30") board = setBoard(data_board) @@ -221,10 +221,10 @@ def api_process(self, path_split): values['stats'] = out except ValueError: values['stats'] = None - raise APIError, "Stats error" + raise APIError("Stats error") values['stats']['reportCount'] = report_count['COUNT(id)'] else: - raise APIError, "Invalid method" + raise APIError("Invalid method") values['time'] = int(t) return json.dumps(values, sort_keys=True, separators=(',', ':')) @@ -313,7 +313,7 @@ def verifyPasswd(username, passwd): except argon2.exceptions.VerifyMismatchError: return None except argon2.exceptions.InvalidHash: - raise UserError, "Hash obsoleto o inválido. Por favor contacte al administrador." + raise UserError("Hash obsoleto o inválido. Por favor contacte al administrador.") if ph.check_needs_rehash(staff_account['password']): param_new_hash = ph.hash(staff_acount['password']) diff --git a/cgi/oekaki.py b/cgi/oekaki.py index e063c61..b5fddf3 100644 --- a/cgi/oekaki.py +++ b/cgi/oekaki.py @@ -1,5 +1,4 @@ # coding=utf-8 -import _mysql import os import cgi import random @@ -42,7 +41,7 @@ def oekaki(self, path_split): board = setBoard(self.formdata['board']) if board['allow_oekaki'] != '1': - raise UserError, 'Esta sección no soporta oekaki.' + raise UserError('Esta sección no soporta oekaki.') # Veamos a quien le estamos respondiendo try: @@ -51,7 +50,7 @@ def oekaki(self, path_split): parentid = 0 # Vemos si el usuario quiere una animacion - if 'oek_animation' in self.formdata.keys(): + if 'oek_animation' in self.formdata: animation = True animation_str = 'animation' else: @@ -63,8 +62,8 @@ def oekaki(self, path_split): width = int(self.formdata['oek_x']) height = int(self.formdata['oek_y']) except: - raise UserError, 'Valores de tamaño inválidos (%s)' % repr( - self.formdata) + raise UserError('Valores de tamaño inválidos (%s)' % repr( + self.formdata)) params = { 'dir_resource': Settings.BOARDS_URL + 'oek_temp/', @@ -88,7 +87,7 @@ def oekaki(self, path_split): 'compress_level': '4' } - if 'oek_edit' in self.formdata.keys(): + if 'oek_edit' in self.formdata: # Si hay que editar, cargar la imagen correspondiente en el canvas pid = int(self.formdata['oek_edit']) post = FetchOne( @@ -106,7 +105,7 @@ def oekaki(self, path_split): params['image_width'] = str(width) params['image_height'] = str(height) - if 'canvas' in self.formdata.keys(): + if 'canvas' in self.formdata: editfile = self.formdata['canvas'] # Darle las dimensiones al exit script @@ -142,7 +141,7 @@ def oekaki(self, path_split): parentid = None if board['allow_oekaki'] != '1': - raise UserError, 'Esta sección no soporta oekaki.' + raise UserError('Esta sección no soporta oekaki.') ts = int(time.time()) ip = inet_aton(self.environ["REMOTE_ADDR"]) @@ -168,7 +167,7 @@ def oekaki(self, path_split): board = setBoard(path_split[3]) file = int(path_split[4]) except: - raise UserError, 'Board o archivo de animación inválido.' + raise UserError('Board o archivo de animación inválido.') params = { 'pch_file': Settings.BOARDS_URL + board['dir'] + '/src/' + str(file) + '.pch', @@ -186,7 +185,7 @@ def oekaki(self, path_split): '
\n' page += '' - for key in params.keys(): + for key in params: page += '' + "\n" page += '
Java must be installed and enabled to use this applet. Please refer to our Java setup tutorial for more information.
' diff --git a/cgi/post.py b/cgi/post.py index 3b836bb..6f7ff03 100644 --- a/cgi/post.py +++ b/cgi/post.py @@ -4,8 +4,7 @@ import os import shutil import time import threading -import Queue -import _mysql +import queue import formatting import logging @@ -34,10 +33,16 @@ class Post(object): "thumb_width": 0, "thumb_height": 0, "ip": "", + "IS_DELETED": 0, "timestamp_formatted": "", "timestamp": 0, "bumped": 0, "locked": 0, + "last": 0, + "expires": 0, + "expires_formatted": "", + "expires_alert": 0, + "length": 0, } def __getitem__(self, key): @@ -51,20 +56,25 @@ class Post(object): def insert(self): logging.info("Insertando Post") + post_keys = [] + post_templates = [] post_values = [] - for key, value in self.post.iteritems(): + for key, value in self.post.items(): + post_keys.append(key) + if key == 'ip': - template = "INET6_ATON('%s')" + template = "INET6_ATON(%s)" else: - template = "'%s'" + template = "%s" - post_values.append(template % _mysql.escape_string(str(value))) + post_templates.append(template) + post_values.append(value) return InsertDb("INSERT INTO `posts` (`%s`) VALUES (%s)" % ( - "`, `".join(self.post.keys()), - ", ".join(post_values) - )) + "`, `".join(post_keys), + ", ".join(post_templates) + ), post_values) class RegenerateThread(threading.Thread): def __init__(self, threadid, request_queue): @@ -89,15 +99,17 @@ def threadNumReplies(post): """ board = Settings._.BOARD - num = FetchOne("SELECT COUNT(1) FROM `posts` WHERE `parentid` = '%s' AND `boardid` = '%s'" % (post, board['id']), 0) - return int(num[0])+1 + num = FetchOne("SELECT COUNT(1) AS 'count' FROM `posts` WHERE `parentid` = %s AND `boardid` = %s" % (post, board['id'])) + return int(num["count"]) + 1 def get_parent_post(post_id, board_id): - post = FetchOne("SELECT `id`, `email`, `message`, `locked`, `subject`, `timestamp`, `bumped`, `last`, `length` FROM `posts` WHERE `id` = %s AND `parentid` = 0 AND `IS_DELETED` = 0 AND `boardid` = %s LIMIT 1" % (post_id, board_id)) + post = FetchOne("SELECT `id`, `email`, `message`, `locked`, `subject`, `timestamp`, `bumped`, `last`, `length` FROM `posts` WHERE `id` = %s AND `parentid` = 0 AND `IS_DELETED` = 0 AND `boardid` = %s LIMIT 1", + (post_id, board_id)) + if post: return post else: - raise UserError, _("The ID of the parent post is invalid.") + raise UserError(_("The ID of the parent post is invalid.")) def getThread(postid=0, mobile=False, timestamp=0): board = Settings._.BOARD @@ -106,11 +118,10 @@ def getThread(postid=0, mobile=False, timestamp=0): database_lock.acquire() try: if timestamp: - cond = "`timestamp` = %s" % str(timestamp) + op_post = FetchOne("SELECT * FROM `posts` WHERE `timestamp` = %s AND `boardid` = %s AND parentid = 0 LIMIT 1", (timestamp, board["id"])) else: - cond = "`id` = %s" % str(postid) + op_post = FetchOne("SELECT * FROM `posts` WHERE `id` = %s AND `boardid` = %s AND parentid = 0 LIMIT 1", (postid, board["id"])) - op_post = FetchOne("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE %s AND `boardid` = %s AND parentid = 0 LIMIT 1" % (cond, board["id"])) if op_post: op_post['num'] = 1 if mobile: @@ -119,7 +130,7 @@ def getThread(postid=0, mobile=False, timestamp=0): #thread = {"id": op_post["id"], "posts": [op_post], "omitted": 0, "omitted_img": 0} total_bytes += len(op_post["message"])+80 - replies = FetchAll("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE `parentid` = %s AND `boardid` = %s ORDER BY `id` ASC" % (op_post["id"], board["id"])) + replies = FetchAll("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE `parentid` = %s AND `boardid` = %s ORDER BY `id` ASC", (op_post["id"], board["id"])) thread["length"] = 1 if replies: for reply in replies: @@ -132,7 +143,7 @@ def getThread(postid=0, mobile=False, timestamp=0): total_bytes += len(reply["message"])+57 # An imageboard needs subject - if board["board_type"] in ['1', '5']: + if board["board_type"] in [1, 5]: thread["timestamp"] = op_post["timestamp"] thread["subject"] = op_post["subject"] thread["message"] = op_post["message"] @@ -141,6 +152,7 @@ def getThread(postid=0, mobile=False, timestamp=0): #threads = [thread] else: + raise Exception(postid) return None finally: database_lock.release() @@ -152,7 +164,7 @@ def getID(threadid, postnum): database_lock.acquire() try: - posts = FetchAll("SELECT id FROM `posts` WHERE `parentid`=%s AND `boardid`=%s ORDER BY `id` ASC" % (thread["id"], board["id"])) + posts = FetchAll("SELECT id FROM `posts` WHERE `parentid`= %s AND `boardid`= %s ORDER BY `id` ASC", (thread["id"], board["id"])) if posts: post = posts[int(postnum)-1] postid = post["id"] @@ -181,10 +193,6 @@ def shortenMsg(message, elid='0', elboard='0'): message_shortened += message_exploded[i] + '
' - #try: - message_shortened = message_shortened.decode('utf-8', 'replace') - #except: - if len(message_shortened) > limit: message_shortened = message_shortened[:limit] @@ -202,7 +210,7 @@ def threadUpdated(postid): """ # Use queues only if multithreading is enabled if Settings.USE_MULTITHREADING: - request_queue = Queue.Queue() + request_queue = queue.Queue() threads = [RegenerateThread(i, request_queue) for i in range(2)] for t in threads: t.start() @@ -226,7 +234,7 @@ def regenerateFrontPages(): """ board = Settings._.BOARD threads = [] - if board['board_type'] == '1': + if board['board_type'] == 1: threads_to_fetch = int(board['numthreads']) threads_to_limit = threads_to_fetch + 50 else: @@ -238,18 +246,16 @@ def regenerateFrontPages(): database_lock.acquire() try: # fetch necessary threads and calculate how many posts we need - allthreads_query = "SELECT id, timestamp, subject, locked, length FROM `posts` WHERE `boardid` = '%s' AND parentid = 0 AND IS_DELETED = 0 ORDER BY `bumped` DESC, `id` ASC LIMIT %d" % \ - (board["id"], threads_to_limit) - allthreads = FetchAll(allthreads_query) + allthreads_query = "SELECT id, timestamp, subject, locked, length FROM `posts` WHERE `boardid` = %s AND parentid = 0 AND IS_DELETED = 0 ORDER BY `bumped` DESC, `id` ASC LIMIT %s" + allthreads = FetchAll(allthreads_query, (board["id"], threads_to_limit)) posts_to_fetch = 0 for t in allthreads[:threads_to_fetch]: posts_to_fetch += int(t["length"]) more_threads = allthreads[threads_to_fetch:50] # get the needed posts for the front page and order them - posts_query = "SELECT * FROM `posts` WHERE `boardid` = '%s' ORDER BY `bumped` DESC, CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC LIMIT %d" % \ - (board["id"], posts_to_fetch) - posts = FetchAll(posts_query) + posts_query = "SELECT * FROM `posts` WHERE `boardid` = %s ORDER BY `bumped` DESC, CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC LIMIT %s" + posts = FetchAll(posts_query, (board["id"], posts_to_fetch)) threads = [] if posts: @@ -257,9 +263,9 @@ def regenerateFrontPages(): post_num = 0 for post in posts: - if post["parentid"] == '0': + if not post["parentid"]: skipThread = False - if post["IS_DELETED"] == '0': + if not post["IS_DELETED"]: # OP; Make new thread if thread is not None: thread["length"] = post_num @@ -284,7 +290,7 @@ def regenerateFrontPages(): is_omitted = False if len(threads) > 0: # Todo : Make this better - if board['board_type'] == '1': + if board['board_type'] == 1: page_count = 1 # Front page only threads_per_page = int(board['numthreads']) else: @@ -299,7 +305,7 @@ def regenerateFrontPages(): page_count = int(math.ceil(float(len(threads)) / float(int(board['numthreads'])))) threads_per_page = int(board['numthreads']) - for i in xrange(page_count): + for i in range(page_count): pages.append([]) start = i * threads_per_page end = start + threads_per_page @@ -351,7 +357,7 @@ def regeneratePage(page_num, page_count, threads, is_omitted=False, more_threads else: file_name = str(page_num) - if board['board_type'] == '1': + if board['board_type'] == 1: templatename = "txt_board.html" else: templatename = "board.html" @@ -384,7 +390,7 @@ def threadList(mode=0): maxthreads = 1000 cutFactor = 70 - if board['board_type'] == '1': + if board['board_type'] == 1: filename = "txt_threadlist.html" full_threads = FetchAll("SELECT id, timestamp, timestamp_formatted, subject, length, last FROM `posts` WHERE parentid = 0 AND boardid = %(board)s AND IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)s" \ % {'board': board["id"], 'limit': maxthreads}) @@ -397,7 +403,7 @@ def threadList(mode=0): # Generate threadlist timestamps = [] for thread in full_threads: - if board['board_type'] == '1': + if board['board_type'] == 1: thread["timestamp_formatted"] = thread["timestamp_formatted"].split(" ")[0] timestamps.append([thread["last"], formatTimestamp(thread["last"])]) if mobile: @@ -443,8 +449,8 @@ def threadList(mode=0): def catalog(sort=''): board = Settings._.BOARD - if board['board_type'] != '0': - raise UserError, "No hay catálogo disponible para esta sección." + if board['board_type'] != 0: + raise UserError("No hay catálogo disponible para esta sección.") cutFactor = 500 @@ -484,7 +490,7 @@ def regenerateThreadPage(postid): thread = getThread(postid) - if board['board_type'] in ['1', '5']: + if board['board_type'] in [1, 5]: template_filename = "txt_thread.html" outname = Settings.ROOT_DIR + board["dir"] + "/res/" + str(thread["timestamp"]) + ".html" title_matome = thread['subject'] @@ -510,14 +516,14 @@ def regenerateThreadPage(postid): def threadPage(postid, mobile=False, timestamp=0): board = Settings._.BOARD - if board['board_type'] in ['1', '5']: + if board['board_type'] in [1, 5]: template_filename = "txt_thread.html" else: template_filename = "board.html" thread = getThread(postid, mobile, timestamp) if not thread: - raise UserError, "El hilo no existe." + raise UserError("El hilo no existe.") return renderTemplate(template_filename, {"threads": [thread], "replythread": postid}, mobile) @@ -525,8 +531,8 @@ def dynamicRead(parentid, ranges, mobile=False): import re board = Settings._.BOARD - if board['board_type'] != '1': - raise UserError, "Esta sección no es un BBS y como tal no soporta lectura dinámica." + if board['board_type'] != 1: + raise UserError("Esta sección no es un BBS y como tal no soporta lectura dinámica.") # get entire thread template_fname = "txt_thread.html" @@ -539,10 +545,10 @@ def dynamicRead(parentid, ranges, mobile=False): import json with open(fname) as f: thread = json.load(f) - thread['posts'] = [dict(zip(thread['keys'], row)) for row in thread['posts']] + thread['posts'] = [dict(list(zip(thread['keys'], row))) for row in thread['posts']] template_fname = "txt_archive.html" else: - raise UserError, 'El hilo no existe.' + raise UserError('El hilo no existe.') filtered_thread = { "id": thread['id'], @@ -645,7 +651,7 @@ def dynamicRead(parentid, ranges, mobile=False): filtered_thread["posts"].insert(0, thread["posts"][0]) if not filtered_thread["posts"]: - raise UserError, "No hay posts que mostrar." + raise UserError("No hay posts que mostrar.") post_preview = cut_home_msg(filtered_thread["posts"][0]["message"], 0) @@ -663,7 +669,7 @@ def regenerateBoard(everything=False): # Use queues only if multithreading is enabled if Settings.USE_MULTITHREADING: - request_queue = Queue.Queue() + request_queue = queue.Queue() threads = [RegenerateThread(i, request_queue) for i in range(Settings.MAX_PROGRAM_THREADS)] for t in threads: t.start() @@ -694,61 +700,62 @@ def deletePost(postid, password, deltype='0', imageonly=False, quick=False): postid = int(postid) # get post - post = FetchOne("SELECT `id`, `timestamp`, `parentid`, `file`, `thumb`, `password`, `length` FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(postid))) + post = FetchOne("SELECT `id`, `timestamp`, `parentid`, `file`, `thumb`, `password`, `length` FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1", (board["id"], postid)) # abort if the post doesn't exist if not post: - raise UserError, _("There isn't a post with this ID. It was probably deleted.") + raise UserError(_("There isn't a post with this ID. It was probably deleted.")) if password: if password != post['password']: - raise UserError, "No tienes permiso para eliminar este mensaje." - elif post["parentid"] == '0' and int(post["length"]) >= Settings.DELETE_FORBID_LENGTH: - raise UserError, "No puedes eliminar un hilo con tantas respuestas." - elif (int(time.time()) - int(post["timestamp"])) > 86400: - raise UserError, "No puedes eliminar un post tan viejo." + raise UserError("No tienes permiso para eliminar este mensaje.") + elif not post["parentid"] and post["length"] >= Settings.DELETE_FORBID_LENGTH: + raise UserError("No puedes eliminar un hilo con tantas respuestas.") + elif (int(time.time()) - post["timestamp"]) > 86400: + raise UserError("No puedes eliminar un post tan viejo.") # just update the DB if deleting only the image, otherwise delete whole post if imageonly: if post["file"]: deleteFile(post) - UpdateDb("UPDATE `posts` SET `file` = '', `file_hex` = '', `thumb` = '', `thumb_width` = 0, `thumb_height` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(post['id']))) + UpdateDb("UPDATE `posts` SET `file` = '', `file_hex` = '', `thumb` = '', `thumb_width` = 0, `thumb_height` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1", (board["id"], post['id'])) else: - if int(post["parentid"]) == 0: + if not post["parentid"]: deleteReplies(post) logging.info("Deleting post " + str(postid)) - if deltype != '0' and post["parentid"] != '0': + if deltype != 0 and post["parentid"]: # Soft delete (recycle bin) - UpdateDb("UPDATE `posts` SET `IS_DELETED` = %s WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (deltype, board["id"], post["id"])) + UpdateDb("UPDATE `posts` SET `IS_DELETED` = %s WHERE `boardid` = %s AND `id` = %s LIMIT 1", (deltype, board["id"], post["id"])) else: # Hard delete if post["file"]: deleteFile(post) - if post['parentid'] != '0': + if post['parentid']: numreplies = threadNumReplies(post["parentid"]) if numreplies > 2: - newlast = FetchOne('SELECT timestamp, email FROM posts WHERE boardid = %s AND parentid = %s AND timestamp < %d ORDER BY timestamp DESC LIMIT 1' % (board['id'], post['parentid'], int(post['timestamp']))) + newlast = FetchOne('SELECT timestamp, email FROM posts WHERE boardid = %s AND parentid = %s AND timestamp < %s ORDER BY timestamp DESC LIMIT 1', (board['id'], post['parentid'], post['timestamp'])) else: - newlast = FetchOne('SELECT timestamp FROM posts WHERE boardid = %s AND id = %s LIMIT 1' % (board['id'], post['parentid'])) + newlast = FetchOne('SELECT timestamp FROM posts WHERE boardid = %s AND id = %s LIMIT 1', (board['id'], post['parentid'])) - UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], post["id"])) + UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1", (board["id"], post["id"])) - if post['parentid'] != '0': - UpdateDb("UPDATE `posts` SET last = %s, length = %d WHERE `id` = '%s' AND `boardid` = '%s'" % (newlast["timestamp"], threadNumReplies(post["parentid"]), post["parentid"], board["id"])) + if post['parentid']: + UpdateDb("UPDATE `posts` SET last = %s, length = %s WHERE `id` = %s AND `boardid` = %s", + (newlast["timestamp"], threadNumReplies(post["parentid"]), post["parentid"], board["id"])) - if post['parentid'] == '0': - if board['board_type'] == '1': - os.unlink(Settings.ROOT_DIR + board["dir"] + "/res/" + post["timestamp"] + ".html") + if not post['parentid']: + if board['board_type'] == 1: + os.unlink(Settings.ROOT_DIR + str(board["dir"]) + "/res/" + str(post["timestamp"]) + ".html") else: - os.unlink(Settings.ROOT_DIR + board["dir"] + "/res/" + post["id"] + ".html") + os.unlink(Settings.ROOT_DIR + str(board["dir"]) + "/res/" + str(post["id"]) + ".html") regenerateHome() # rebuild thread and fronts if reply; rebuild only fronts if not - if post["parentid"] != '0': + if post["parentid"]: threadUpdated(post["parentid"]) else: regenerateFrontPages() @@ -757,12 +764,12 @@ def deleteReplies(thread): board = Settings._.BOARD # delete files first - replies = FetchAll("SELECT `parentid`, `file`, `thumb` FROM `posts` WHERE `boardid` = %s AND `parentid` = %s AND `file` != ''" % (board["id"], thread["id"])) + replies = FetchAll("SELECT `parentid`, `file`, `thumb` FROM `posts` WHERE `boardid` = %s AND `parentid` = %s AND `file` != ''", (board["id"], thread["id"])) for post in replies: deleteFile(post) # delete all replies from DB - UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `parentid` = %s" % (board["id"], thread["id"])) + UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `parentid` = %s", (board["id"], thread["id"])) def deleteFile(post): """ @@ -804,7 +811,7 @@ def trimThreads(): archived = False # Use limit of the board type - if board['board_type'] == '1': + if board['board_type'] == 1: limit = Settings.TXT_MAX_THREADS else: limit = Settings.MAX_THREADS @@ -815,36 +822,36 @@ def trimThreads(): alert_time = int(round(int(board['maxage']) * Settings.MAX_AGE_ALERT)) time_limit = t + (alert_time * 86400) - old_ops = FetchAll("SELECT `id`, `timestamp`, `expires`, `expires_alert`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `expires` > 0 AND `expires` < %s LIMIT 50" % (board['id'], time_limit)) + old_ops = FetchAll("SELECT `id`, `timestamp`, `expires`, `expires_alert`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `expires` > 0 AND `expires` < %s LIMIT 50", (board['id'], time_limit)) for op in old_ops: if t >= int(op['expires']): # Trim old threads - if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + if board['archive'] and op["length"] >= Settings.ARCHIVE_MIN_LENGTH: archiveThread(op["id"]) archived = True deletePost(op["id"], None) else: # Add alert to threads approaching deletion - UpdateDb("UPDATE `posts` SET expires_alert = 1 WHERE `boardid` = %s AND `id` = %s" % (board['id'], op['id'])) + UpdateDb("UPDATE `posts` SET expires_alert = 1 WHERE `boardid` = %s AND `id` = %s", (board['id'], op['id'])) # trim inactive threads next - if board['maxinactive'] != '0': + if board['maxinactive'] > 0: t = time.time() oldest_last = t - (int(board['maxinactive']) * 86400) - old_ops = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `last` < %d LIMIT 50" % (board['id'], oldest_last)) + old_ops = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `last` < %s LIMIT 50", (board['id'], oldest_last)) for op in old_ops: - if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + if board['archive'] and op["length"] >= Settings.ARCHIVE_MIN_LENGTH: archiveThread(op["id"]) archived = True deletePost(op["id"], None) # select trim type by board - if board['board_type'] == '1': + if board['board_type'] == 1: trim_method = Settings.TXT_TRIM_METHOD else: trim_method = Settings.TRIM_METHOD @@ -862,7 +869,7 @@ def trimThreads(): if len(op_posts) > limit: posts = op_posts[limit:] for post in posts: - if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + if board['archive'] and op["length"] >= Settings.ARCHIVE_MIN_LENGTH: archiveThread(post["id"]) archived = True @@ -879,7 +886,7 @@ def autoclose_thread(parentid, t, replies): board = Settings._.BOARD # decide the replylimit - if board['board_type'] == '1' and Settings.TXT_CLOSE_THREAD_ON_REPLIES > 0: + if board['board_type'] == 1 and Settings.TXT_CLOSE_THREAD_ON_REPLIES > 0: replylimit = Settings.TXT_CLOSE_THREAD_ON_REPLIES elif Settings.CLOSE_THREAD_ON_REPLIES > 0: replylimit = Settings.CLOSE_THREAD_ON_REPLIES @@ -929,7 +936,7 @@ def pageNavigator(page_num, page_count, is_omitted=False): pagenav += "" - for i in xrange(page_count): + for i in range(page_count): if i == page_num: pagenav += "[%d]" % i else: @@ -965,18 +972,20 @@ def flood_check(t,post,boardid): #lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = INET6_ATON('%s') and `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, int(maxtime)), 0) # NO MATTER THE IP - lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (boardid, int(maxtime)), 0) + lastpost = FetchOne("SELECT `timestamp` FROM `posts` WHERE `parentid` = 0 and `boardid` = %s and IS_DELETED = 0 AND timestamp > %s", + (boardid, maxtime)) else: maxtime = round(t - int(board['postsecs'])) - lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = INET6_ATON('%s') and `parentid` != 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, int(maxtime)), 0) + lastpost = FetchOne("SELECT `timestamp` FROM `posts` WHERE `ip` = INET6_ATON(%s) and `parentid` != 0 and `boardid` = %s and IS_DELETED = 0 AND timestamp > %s", + (post["ip"], boardid, maxtime)) - if int(lastpost[0]): + if lastpost: if post["parentid"]: - raise UserError, _("Flood detected. Please wait a moment before posting again.") + raise UserError(_("Flood detected. Please wait a moment before posting again.")) else: - lastpost = FetchOne("SELECT `timestamp` FROM `posts` WHERE `parentid`=0 and `boardid`='%s' and IS_DELETED = 0 ORDER BY `timestamp` DESC" % (boardid), 0) - wait = int(int(board['threadsecs']) - (t - int(lastpost[0]))) - raise UserError, "Espera " + str(wait) + " segundos antes de crear otro hilo." + lastpost = FetchOne("SELECT `timestamp` FROM `posts` WHERE `parentid`= 0 and `boardid`= %s and IS_DELETED = 0 ORDER BY `timestamp` DESC", (boardid,)) + wait = int(int(board['threadsecs']) - (t - int(lastpost["timestamp"]))) + raise UserError("Espera " + str(wait) + " segundos antes de crear otro hilo.") def cut_home_msg(message, boardlength=0): short_message = message.replace("
", " ") @@ -985,17 +994,15 @@ def cut_home_msg(message, boardlength=0): limit = Settings.HOME_LASTPOSTS_LENGTH - boardlength if len(short_message) > limit: - if isinstance(short_message, unicode): - short_message = short_message[:limit].encode('utf-8') + "…" - else: - short_message = short_message.decode('utf-8')[:limit].encode('utf-8') + "…" + if isinstance(short_message, str): + short_message = short_message[:limit] + "…" short_message = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub("", short_message) # Removes incomplete HTML return short_message def getLastAge(board_type, limit): threads = [] - sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, bumped, last, length, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND posts.locked < 3 AND boards.board_type = %d ORDER BY bumped DESC LIMIT %d" % (board_type, limit) - threads = FetchAll(sql) + sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, bumped, last, length, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND posts.locked < 3 AND boards.board_type = %s ORDER BY bumped DESC LIMIT %s" + threads = FetchAll(sql, (board_type, limit)) for post in threads: post['id'] = int(post['id']) @@ -1015,8 +1022,8 @@ def getLastAge(board_type, limit): def getNewThreads(limit): threads = [] - sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND boards.id <> 34 AND boards.id <> 13 AND posts.locked = 0 ORDER BY timestamp DESC LIMIT %d" % (limit) - threads = FetchAll(sql) + sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND boards.id <> 34 AND boards.id <> 13 AND posts.locked = 0 ORDER BY timestamp DESC LIMIT %s" + threads = FetchAll(sql, (limit,)) for post in threads: post['id'] = int(post['id']) @@ -1092,13 +1099,16 @@ def regenerateAccess(): if not Settings.HTACCESS_GEN: return False - bans = FetchAll("SELECT `ipstr`, `boards` FROM `bans` WHERE `blind` = '1' ORDER BY `ipstart` ASC") boards = FetchAll('SELECT `dir` FROM `boards`') global_boards = [board['dir'] for board in boards if board['dir'] not in Settings.EXCLUDE_GLOBAL_BANS] + bans = [] global_bans = [] board_bans = {} + + if Settings.ENABLE_BANS: + bans = FetchAll("SELECT `ipstr`, `boards` FROM `bans` WHERE `blind` = '1' ORDER BY `ipstart` ASC") for ban in bans: if ban["boards"]: @@ -1159,14 +1169,14 @@ def make_url(postid, post, parent_post, noko, mobile): if mobile: if not noko: url = Settings.CGI_URL + 'mobile/' + board["dir"] - elif board["board_type"] == '1': + elif board["board_type"] == 1: url = "%s/mobileread/%s/%s/l10#form" % (Settings.CGI_URL, board["dir"], parent_post['timestamp']) else: url = "%s/mobileread/%s/%s#%s" % (Settings.CGI_URL, board["dir"], parentid, postid) else: if not noko: url = Settings.BOARDS_URL + board["dir"] + "/" - elif board["board_type"] == '1': + elif board["board_type"] == 1: url = "%s/read/%s/l50#bottom" % (Settings.BOARDS_URL + board["dir"], str(parent_post['timestamp'])) else: url = "%s/res/%s.html#%s" % (Settings.BOARDS_URL + board["dir"], str(parentid), postid) @@ -1192,7 +1202,7 @@ def latestAdd(post, postnum, postid, parent_post): timestamp_formatted = datetime.datetime.fromtimestamp(post['timestamp']).strftime('%Y-%m-%dT%H:%M:%S%Z') parentid = parent_post['id'] if post['parentid'] else postid - if board['board_type'] == '1': + if board['board_type'] == 1: url = '/%s/read/%s/%d' % (board['dir'], (parent_post['timestamp'] if post['parentid'] else post['timestamp']), (postnum if postnum else 1)) else: url = '/%s/res/%s.html#%s' % (board['dir'], parentid, postid) @@ -1221,19 +1231,19 @@ def archiveThread(postid): with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".json", "w") as f: json.dump(thread, f, indent=0) except: - raise UserError, "Can't archive: %s" % thread['timestamp'] + raise UserError("Can't archive: %s" % thread['timestamp']) UpdateDb("REPLACE INTO archive (id, boardid, timestamp, subject, length) VALUES ('%s', '%s', '%s', '%s', '%s')" % (thread['id'], board['id'], thread['timestamp'], _mysql.escape_string(thread['subject']), thread['length'])) def throw_dice(dice): qty = int(dice[0][1:]) if qty == 0: - raise UserError, "No tienes dados para lanzar." + raise UserError("No tienes dados para lanzar.") if qty > 100: qty = 100 sides = int(dice[1][1:]) if dice[1] else 6 if sides == 0: - raise UserError, "Tus dados no tienen caras." + raise UserError("Tus dados no tienen caras.") if sides > 100: sides = 100 @@ -1262,7 +1272,7 @@ def discord_hook(post, url): if not Settings.DISCORD_HOOK_URL: return - import urllib2 + import urllib.request, urllib.error, urllib.parse import json board = Settings._.BOARD @@ -1283,7 +1293,7 @@ def discord_hook(post, url): data = {"content": "test"} jsondata = json.dumps(data, separators=(',',':')) - opener = urllib2.build_opener() + opener = urllib.request.build_opener() #opener.addheaders = [('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0')] response = opener.open(Settings.DISCORD_HOOK_URL, jsondata, 6) the_page = response.read() diff --git a/cgi/template.py b/cgi/template.py index f58e17f..8c3fc19 100644 --- a/cgi/template.py +++ b/cgi/template.py @@ -69,19 +69,19 @@ def renderTemplate(template, template_values={}, mobile=False, noindex=False): "board_long": board["longname"], "board_type": board["board_type"], "oek_finish": 0, - "disable_name": (board["disable_name"] == '1'), - "disable_subject": (board["disable_subject"] == '1'), + "disable_name": board["disable_name"], + "disable_subject": board["disable_subject"], "default_subject": board["subject"], "postarea_desc": board["postarea_desc"], "postarea_extra": board["postarea_extra"], - "allow_images": (board["allow_images"] == '1'), - "allow_image_replies": (board["allow_image_replies"] == '1'), - "allow_noimage": (board["allow_noimage"] == '1'), - "allow_spoilers": (board["allow_spoilers"] == '1'), - "allow_oekaki": (board["allow_oekaki"] == '1'), - "archive": (board["archive"] == '1'), + "allow_images": board["allow_images"], + "allow_image_replies": board["allow_image_replies"], + "allow_noimage": board["allow_noimage"], + "allow_spoilers": board["allow_spoilers"], + "allow_oekaki": board["allow_oekaki"], + "archive": board["archive"], "force_css": board["force_css"], - "noindex": (board["secret"] == '1'), + "noindex": board["secret"], "useid": board["useid"], "maxsize": board["maxsize"], "maxage": board["maxage"], diff --git a/cgi/templates/home.html b/cgi/templates/home.html index bc37431..04fbf72 100644 --- a/cgi/templates/home.html +++ b/cgi/templates/home.html @@ -8,9 +8,9 @@ Bienvenido a Internet BBS/IB - + - + @@ -54,7 +54,6 @@
Frame · BaI Móvil · - Onion · BaI TV · BaI Radio · Estadísticas diff --git a/cgi/templates/manage/boardoptions.html b/cgi/templates/manage/boardoptions.html index 436b036..fcd3bb8 100644 --- a/cgi/templates/manage/boardoptions.html +++ b/cgi/templates/manage/boardoptions.html @@ -38,7 +38,7 @@
@@ -74,9 +74,9 @@ @@ -85,9 +85,9 @@ @@ -96,45 +96,45 @@ - + - + - + - + - + - + - + - + - + diff --git a/cgi/templates/manage/ipshow.html b/cgi/templates/manage/ipshow.html index 6937a0e..4e9de17 100644 --- a/cgi/templates/manage/ipshow.html +++ b/cgi/templates/manage/ipshow.html @@ -49,14 +49,14 @@ diff --git a/cgi/templates/revision.html b/cgi/templates/revision.html index ef50561..78bc1ab 100644 --- a/cgi/templates/revision.html +++ b/cgi/templates/revision.html @@ -1 +1 @@ -0.8.10 +0.10.0 diff --git a/cgi/templates/txt_board.html b/cgi/templates/txt_board.html index 7b0207b..375613a 100644 --- a/cgi/templates/txt_board.html +++ b/cgi/templates/txt_board.html @@ -45,9 +45,9 @@

[#{titer}:#{thread['length']}]#{thread['posts'][0]['subject']}

- +

#{post['num']} : Mensaje eliminado por usuario.

- +

#{post['num']} : Mensaje eliminado por staff.

@@ -77,7 +77,7 @@
- +
diff --git a/cgi/templates/txt_thread.html b/cgi/templates/txt_thread.html index c8d70d1..d2d3224 100644 --- a/cgi/templates/txt_thread.html +++ b/cgi/templates/txt_thread.html @@ -9,7 +9,7 @@ 100: ?> Primeros 100 - + #{(i+1)*100+1}- 51: ?> @@ -28,9 +28,9 @@

#{thread['subject']} (${(str(thread['length'])+" respuestas") if thread['length']>1 else "Una respuesta"})

- +

#{post['num']} : Mensaje eliminado por usuario.

- +

#{post['num']} : Mensaje eliminado por staff.

@@ -59,7 +59,7 @@
#{thread['size']}

- +
@@ -84,7 +84,7 @@ ▲Subir▲
- +
Nombre: E-mail:

@@ -103,4 +103,4 @@ - \ No newline at end of file + diff --git a/cgi/weabot.py b/cgi/weabot.py index 38efa7c..720916d 100755 --- a/cgi/weabot.py +++ b/cgi/weabot.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # coding=utf-8 # Remove the first line to use the env command to locate python @@ -9,8 +9,8 @@ import datetime import random import cgi import logging -import _mysql -from Cookie import SimpleCookie +from MySQLdb import _mysql +from http.cookies import SimpleCookie import tenjin import manage @@ -23,7 +23,7 @@ from formatting import * from post import * from img import * -__version__ = "0.8.10" +__version__ = "0.10.0" # Set to True to disable weabot's exception routing and enable profiling _DEBUG = False @@ -35,7 +35,7 @@ class weabot(object): def __init__(self, environ, start_response): global _DEBUG - logging.basicConfig(filename='weabot.log', format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) + logging.basicConfig(filename='weabot.log', format='%(asctime)s %(levelname)s %(message)s', level=logging.DEBUG) self.environ = environ if self.environ["PATH_INFO"].startswith("/weabot.py/"): @@ -61,9 +61,9 @@ class weabot(object): else: try: self.run() - except UserError, message: + except UserError as message: self.error(message) - except Exception, inst: + except Exception as inst: logging.exception(inst) import sys @@ -74,11 +74,12 @@ class weabot(object): self.exception(type(inst), inst, detail) # close database and finish - CloseDb() + #CloseDb() def __iter__(self): self.handleResponse() self.start("200 OK", self.headers) + self.output = self.output.encode('utf-8') yield self.output def error(self, message): @@ -115,7 +116,7 @@ class weabot(object): def handleResponse(self): if self._newcookies: - for newcookie in self._newcookies.values(): + for newcookie in list(self._newcookies.values()): self.headers.append( ("Set-Cookie", newcookie.output(header=""))) @@ -129,8 +130,8 @@ class weabot(object): caught = False if Settings.FULL_MAINTENANCE: - raise UserError, _( - "%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE + raise UserError(_( + "%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE) if len(path_split) > 1: if path_split[1] == "post": @@ -138,7 +139,7 @@ class weabot(object): caught = True if 'password' not in self.formdata: - raise UserError, "El request está incompleto." + raise UserError("El request está incompleto.") # let's get all the POST data we need ip = self.environ["REMOTE_ADDR"] @@ -156,7 +157,7 @@ class weabot(object): oek_file = self.formdata.get('oek_file') password = self.formdata.get('password', '') noimage = self.formdata.get('noimage') - mobile = ("mobile" in self.formdata.keys()) + mobile = ("mobile" in self.formdata) # call post function (post_url, ttaken, unused) = self.make_post(ip, boarddir, parent, trap1, trap2, name, @@ -202,7 +203,7 @@ class weabot(object): board = setBoard(path_split[2]) caught = True if board['board_type'] != '1': - raise UserError, "No disponible para esta sección." + raise UserError("No disponible para esta sección.") self.output = threadList(0) elif path_split[1] == "mobile": OpenDb() @@ -261,18 +262,15 @@ class weabot(object): caught = True # Redirect to ban page if user is banned - if addressIsBanned(self.environ['REMOTE_ADDR'], board["dir"], blind_only=True): - raise UserError, '' % board["dir"] + if Settings.ENABLE_BANS and addressIsBanned(self.environ['REMOTE_ADDR'], board["dir"], blind_only=True): + raise UserError('' % board["dir"]) if len(path_split) > 4 and path_split[4] and board['board_type'] == '1': - # try: self.output = dynamicRead(int(path_split[3]), path_split[4], True) - # except: - # self.output = threadPage(path_split[3], True) - elif board['board_type'] == '1': - self.output = threadPage(0, True, path_split[3]) + elif board['board_type'] == 1: + self.output = threadPage(0, True, int(path_split[3])) else: - self.output = threadPage(path_split[3], True) + self.output = threadPage(int(path_split[3]), True) elif path_split[1] == "catalog": OpenDb() board = setBoard(path_split[2]) @@ -334,15 +332,15 @@ class weabot(object): bans = FetchAll("SELECT * FROM `bans` WHERE INET6_ATON('"+self.environ["REMOTE_ADDR"]+"') BETWEEN `ipstart` AND `ipend`") if bans: for ban in bans: - if ban["boards"] != "": + if ban["boards"]: boards = pickle.loads(ban["boards"]) - if ban["boards"] == "" or path_split[2] in boards: + if ban["boards"] or path_split[2] in boards: caught = True if ban["boards"]: boards_str = '/' + '/, /'.join(boards) + '/' else: boards_str = 'todas' - if ban["until"] != "0": + if ban["until"]: expire = formatTimestamp(ban["until"]) else: expire = "" @@ -374,8 +372,8 @@ class weabot(object): board = setBoard(path_split[2]) # Redirect to ban page if user is banned - if addressIsBanned(self.environ['REMOTE_ADDR'], board["dir"], blind_only=True): - raise UserError, '' % board["dir"] + if Settings.ENABLE_BANS and addressIsBanned(self.environ['REMOTE_ADDR'], board["dir"], blind_only=True): + raise UserError('' % board["dir"]) self.output = dynamicRead(int(path_split[3]), path_split[4]) elif path_split[1] == "preview": @@ -386,7 +384,7 @@ class weabot(object): message = format_post( self.formdata["message"], self.environ["REMOTE_ADDR"], self.formdata["parentid"]) self.output = message - except Exception, messagez: + except Exception as messagez: self.output = "Error: " + \ str(messagez) + " : " + str(self.formdata) elif path_split[1] == "mod": @@ -400,11 +398,16 @@ class weabot(object): # Redirect the user back to the front page self.output += '

--> --> -->

' % Settings.HOME_URL + CloseDb() + def make_post(self, ip, boarddir, parent, trap1, trap2, name, email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile): - _STARTTIME = time.clock() # Comment if not debug + _STARTTIME = time.process_time() # Comment if not debug - if hostIsBanned(ip): - raise UserError, "Sufijo de host en lista negra." + if Settings.PROXY_BANS and ip not in Settings.PROXY_WHITELIST and (boarddir not in Settings.EXCLUDE_GLOBAL_BANS): + if addressIsTor(ip) or addressIsProxy(ip) or addressIsBannedCountry(ip) or not addressIsES(ip): + raise UserError("Proxy prohibido.") + if hostIsBanned(ip): + raise UserError("Sufijo de host en lista negra.") # open database OpenDb() @@ -412,25 +415,21 @@ class weabot(object): # set the board board = setBoard(boarddir) - if Settings.PROXY_BAN and (board["dir"] not in Settings.EXCLUDE_GLOBAL_BANS): - if addressIsTor(ip) or addressIsProxy(ip) or addressIsBannedCountry(ip) or not addressIsES(ip): - raise UserError, "Proxy prohibido." - # check length of fields if len(name) > 50: - raise UserError, "El campo de nombre es muy largo." + raise UserError("El campo de nombre es muy largo.") if len(email) > 50: - raise UserError, "El campo de e-mail es muy largo." + raise UserError("El campo de e-mail es muy largo.") if len(subject) > 100: - raise UserError, "El campo de asunto es muy largo." + raise UserError("El campo de asunto es muy largo.") if len(message) > 8000: - raise UserError, "El campo de mensaje es muy largo." + raise UserError("El campo de mensaje es muy largo.") if message.count('\n') > 50: - raise UserError, "El mensaje tiene muchos saltos de línea." + raise UserError("El mensaje tiene muchos saltos de línea.") # anti-spam trap if trap1 or trap2: - raise UserError, "Te quedan tres días de vida." + raise UserError("Te quedan tres días de vida.") # Create a single datetime now so everything syncs up t = time.time() @@ -441,14 +440,14 @@ class weabot(object): regenerateAccess() # Redirect to ban page if user is banned - if addressIsBanned(ip, board["dir"]): - raise UserError, '' % board["dir"] + if Settings.ENABLE_BANS and addressIsBanned(ip, board["dir"]): + raise UserError('' % board["dir"]) # Disallow posting if the site OR board is in maintenance if Settings.MAINTENANCE and board["dir"] != 'polka': - raise UserError, _("%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE + raise UserError(_("%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE) if board["locked"] == '1': - raise UserError, _("This board is closed. You can't post in it.") + raise UserError(_("This board is closed. You can't post in it.")) # create post object post = Post(board["id"]) @@ -465,7 +464,7 @@ class weabot(object): post["parentid"] = parent_post['id'] post["bumped"] = parent_post['bumped'] if parent_post['locked'] == '1': - raise UserError, _("The thread is closed. You can't post in it.") + raise UserError(_("The thread is closed. You can't post in it.")) # check if the user is flooding flood_check(t, post, board["id"]) @@ -550,10 +549,10 @@ class weabot(object): # compatibility : old id function if 'id' in parent_post["email"]: - board["useid"] = '3' + board["useid"] = 3 if 'id' in post["email"]: - board["useid"] = '3' + board["useid"] = 3 if extend: try: @@ -591,35 +590,35 @@ class weabot(object): tim = post["timestamp"] # make ID hash - if board["useid"] != '0': + if board["useid"]: post["timestamp_formatted"] += ' ID:' + iphash(ip, post, tim, board["useid"], mobile, self.environ["HTTP_USER_AGENT"], cap_id, hide_end, (board["countrycode"] in ['1', '2'])) # use for future file checks - xfile = (file or oek_file) + xfile = (file is not None or oek_file) # textboard inforcements (change it to settings maybe?) - if board['board_type'] == '1': + if board['board_type'] == 1: if not post["parentid"] and not post["subject"]: - raise UserError, _( - "You must enter a title to create a thread.") + raise UserError(_( + "You must enter a title to create a thread.")) if not post["message"]: - raise UserError, _("Please enter a message.") + raise UserError(_("Please enter a message.")) else: if not post["parentid"] and not xfile and not noimage: - raise UserError, _( - "You must upload an image first to create a thread.") + raise UserError(_( + "You must upload an image first to create a thread.")) if not xfile and not post["message"]: - raise UserError, _( - "Please enter a message or upload an image to reply.") + raise UserError(_( + "Please enter a message or upload an image to reply.")) # check if this post is allowed if post["parentid"]: - if file and board['allow_image_replies'] == '0': - raise UserError, _("Image replies not allowed.") + if file and not board['allow_image_replies']: + raise UserError(_("Image replies not allowed.")) else: - if file and board['allow_images'] == '0': - raise UserError, _("No images allowed.") + if file and not board['allow_images']: + raise UserError(_("No images allowed.")) # use default values when missing / remove sage from wrong fields if (not post["name"] and not post["tripcode"]) or (post["name"].lower() == 'sage'): @@ -637,7 +636,7 @@ class weabot(object): with open(fname, 'rb') as f: file = f.read() except: - raise UserError, "Imposible leer la imagen oekaki." + raise UserError("Imposible leer la imagen oekaki.") if file and not noimage: post = processImage(post, file, t, file_original, @@ -700,7 +699,7 @@ class weabot(object): host = getHost(ip) if host: - for k, v in isps.iteritems(): + for k, v in isps.items(): if k in host: host_nick = v break @@ -708,7 +707,7 @@ class weabot(object): slips.append(host_nick) # hash - if board["slip"] in ['1', '3']: + if board["slip"] in [1, 3]: if hide_end: slips.append( '-'.join((getMD5(ip + post["name"])[:4], '****'))) @@ -720,7 +719,7 @@ class weabot(object): '-'.join((getMD5(ip)[:4], getMD5(self.environ["HTTP_USER_AGENT"])[:4]))) # host - if board["slip"] == '2': + if board["slip"] == 2: if hide_end: host = '★' else: @@ -743,7 +742,7 @@ class weabot(object): slips.append(host) # IP - if board["slip"] == '3': + if board["slip"] == 3: if hide_end: host = '[*.*.*.*]' else: @@ -755,7 +754,7 @@ class weabot(object): post["tripcode"] += " (%s)" % ' '.join(slips) # country code - if board["countrycode"] == '1': + if board["countrycode"] == 1: if hide_end or addressIsTor(ip): country = '??' else: @@ -763,7 +762,7 @@ class weabot(object): post["name"] += " [%s]" % country # set expiration date if necessary - if board["maxage"] != '0' and not post["parentid"]: + if board["maxage"] != 0 and not post["parentid"]: if board["dir"] == '2d': date_format = '%m月%d日' date_format_y = '%Y年%m月' @@ -784,7 +783,7 @@ class weabot(object): if board["dir"] == 'noticias': # check if there's at least one link if "' + \ - '
'.join(msgs) + raise UserError('No todos los mensajes pudieron ser eliminados.
' + \ + '
'.join(msgs)) # redirect if imageonly: @@ -921,7 +922,7 @@ class weabot(object): def report(self, ip, boarddir, postid, reason, txt, postshow): # don't allow if the report system is off if not Settings.REPORTS_ENABLE: - raise UserError, _('Report system is deactivated.') + raise UserError(_('Report system is deactivated.')) # if there's not a reason, show the report page if reason is None: @@ -931,9 +932,9 @@ class weabot(object): # check reason if not reason: - raise UserError, _("Enter a reason.") + raise UserError(_("Enter a reason.")) if len(reason) > 100: - raise UserError, _("Text too long.") + raise UserError(_("Text too long.")) # open database OpenDb() @@ -942,14 +943,14 @@ class weabot(object): board = setBoard(boarddir) # check if he's banned - if addressIsBanned(ip, board["dir"]): - raise UserError, _("You're banned.") + if Settings.ENABLE_BANS and addressIsBanned(ip, board["dir"]): + raise UserError(_("You're banned.")) # check if post exists post = FetchOne("SELECT `id`, `parentid`, `ip` FROM `posts` WHERE `id` = '%s' AND `boardid` = '%s'" % ( _mysql.escape_string(str(postid)), _mysql.escape_string(board['id']))) if not post: - raise UserError, _("Post doesn't exist.") + raise UserError(_("Post doesn't exist.")) # generate link if board["board_type"] == '1': @@ -989,33 +990,33 @@ class weabot(object): OpenDb() # 1 week = 604800 - query_day = FetchAll("SELECT DATE_FORMAT(FROM_UNIXTIME(FLOOR((timestamp-10800)/86400)*86400+86400), \"%Y-%m-%d\"), COUNT(1), COUNT(IF(parentid=0, 1, NULL)) " + query_day = FetchAll("SELECT DATE_FORMAT(FROM_UNIXTIME(FLOOR((timestamp-10800)/86400)*86400+86400), \"%Y-%m-%d\") AS date, COUNT(1) AS count, COUNT(IF(parentid=0, 1, NULL)) AS threads " "FROM posts " "WHERE (timestamp-10800) > (UNIX_TIMESTAMP()-604800) AND (IS_DELETED = 0) " "GROUP BY FLOOR((timestamp-10800)/86400) " - "ORDER BY FLOOR((timestamp-10800)/86400)", 0) + "ORDER BY FLOOR((timestamp-10800)/86400)") - query_count = FetchOne("SELECT COUNT(1), COUNT(NULLIF(file, '')), VERSION() FROM posts", 0) - total = int(query_count[0]) - total_files = int(query_count[1]) - mysql_ver = query_count[2] + query_count = FetchOne("SELECT COUNT(1) AS posts, COUNT(NULLIF(file, '')) AS files, VERSION() AS version FROM posts") + total = query_count["posts"] + total_files = query_count["files"] + mysql_ver = query_count["version"] - archive_count = FetchOne("SELECT SUM(length) FROM archive", 0) - total_archived = int(archive_count[0]) + archive_count = FetchOne("SELECT SUM(length) AS sum FROM archive") + total_archived = int(archive_count["sum"]) days = [] - for date, count, threads in query_day[1:]: - days.append((date, count, threads)) + for r in query_day[1:]: + days.append((r["date"], r["count"], r["threads"])) - query_b = FetchAll("SELECT id, dir, name FROM boards WHERE boards.secret = 0", 0) + query_b = FetchAll("SELECT id, dir, name FROM boards WHERE boards.secret = 0") boards = [] totalp = 0 - for id, dir, longname in query_b: - bposts = FetchOne("SELECT COUNT(1) FROM posts " - "WHERE '"+str(id)+"' = posts.boardid AND timestamp > ( UNIX_TIMESTAMP(DATE(NOW())) - 2419200 )", 0) - boards.append((dir, longname, int(bposts[0]))) - totalp += int(bposts[0]) + for b in query_b: + bposts = FetchOne("SELECT COUNT(1) AS count FROM posts " + "WHERE posts.boardid = %s AND timestamp > ( UNIX_TIMESTAMP(DATE(NOW())) - 2419200 )", (b['id'],)) + boards.append((b['dir'], b['name'], bposts["count"])) + totalp += bposts["count"] boards = sorted(boards, key=lambda boards: boards[2], reverse=True) @@ -1069,7 +1070,7 @@ class weabot(object): if __name__ == "__main__": - from fcgi import WSGIServer + from flup.server.fcgi import WSGIServer # Psyco is not required, however it will be used if available try: @@ -1084,4 +1085,6 @@ if __name__ == "__main__": except: pass - WSGIServer(weabot).run() + app = WSGIServer(weabot, debug=True, forceCGI=False) + app.run() + -- cgit v1.2.1-18-gbd029
Desactivar nombre
Desactivar asunto
Papelera de reciclaje
Cerrado
Secreto
Permitir spoilers
Permitir oekaki
Permitir crear hilos sin imagen
Permitir subida

Tipos de archivo - - Eliminar - - Rec + + Rec [1] - + Rec [2] + + Eliminar