aboutsummaryrefslogblamecommitdiff
path: root/cgi/modapi.py
blob: 7bb63fbda6b2d289d2dc0df42c081ff553dcc15e (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11










                          

                           
                                                       






















                                                                                      






























                                                                                                                    

                                             

                                               
         



                                                                                
                              
                                                   

                                       
                                                                                                                                                                                                                                                             
                                                                                                                           






                                                          
                                                      

                                             
                                             









                                                                                                                                                                                                                                                                    
                                                          
                                       



































                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
























                                                                                                                                                                                                                                                                                                                                                   
                                                              




















                                                                            
                                 





                                                                     
                                             



                                                                                                                                                                                                  







                                                                                  
                                   





                                                                      






                                                  
                                                                      

                                            





































































                                                                                                         

























                                                                                                          



                          
# coding=utf-8
import json
import _mysql
import time

from framework import *
from database import *
from post import *


def api(self, path_split):
    if len(path_split) > 2:
        try:
            self.output = api_process(self, path_split)
        except APIError, e:
            self.output = api_error("error", e.message)
        except UserError, e:
            self.output = api_error("failed", e.message)
        except Exception, e:
            import sys
            import traceback
            exc_type, exc_value, exc_traceback = sys.exc_info()
            detail = ["%s : %s : %s : %s" % (os.path.basename(
                o[0]), o[1], o[2], o[3]) for o in traceback.extract_tb(exc_traceback)]

            self.output = api_error("exception", str(e), str(type(e)), detail)
    else:
        self.output = api_error("error", "No method specified")


def api_process(self, path_split):
    formdata = self.formdata
    ip = self.environ["REMOTE_ADDR"]
    t = time.time()
    method = path_split[2]
    values = {'state': 'success'}

    validated = False
    staff_account = None

    token = formdata.get("token")
    if token:
        staff_account = validateSession(token)
        if not staff_account:
            self.output = api_error("error", "Session expired")

    if staff_account:
        validated = True
        if 'session_id' in staff_account:
            renewSession(staff_account['session_id'])

        UpdateDb('UPDATE `staff` SET `lastactive` = ' + str(timestamp()
                                                            ) + ' WHERE `id` = ' + staff_account['id'] + ' LIMIT 1')

    if validated == False:
        if method == 'login':
            if 'username' in self.formdata and 'password' in self.formdata:
                staff_account = verifyPasswd(
                    formdata.get("username"), formdata.get("password"))
                if staff_account:
                    session_uuid = newSession(staff_account['id'])
                    values["token"] = session_uuid
                    UpdateDb('DELETE FROM `logs` WHERE `timestamp` < ' +
                             str(timestamp() - 604800))  # one week
                else:
                    logAction('', 'Failed log-in. Username:'+_mysql.escape_string(
                        self.formdata['username'])+' IP:'+self.environ["REMOTE_ADDR"])
                    raise APIError, "Incorrect username/password."
            else:
                raise APIError, "Bad request"
        else:
            raise APIError, "Not authenticated"
    else:
        if method == 'news':
            news = FetchAll(
                "SELECT * FROM `news` WHERE type = 1 ORDER BY `timestamp` DESC")
            values['news'] = news
        elif method == 'post':
            board = setBoard(formdata.get("board"))
            if 'id' in formdata.keys():
                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"]) + "'")
                post['id'] = int(post['id'])
                post['bumped'] = int(post['bumped'])
                post['deleted'] = int(post['deleted'])
                post['last'] = int(post['last'])
                post['locked'] = int(post['locked'])
                post['parentid'] = int(post['parentid'])
                post['timestamp'] = int(post['timestamp'])
                post['boardid'] = int(post['boardid'])
                values['post'] = post
            if 'parentid' in formdata.keys():
                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"]) + "'")
                for post in posts:
                    post['id'] = int(post['id'])
                    post['bumped'] = int(post['bumped'])
                    post['deleted'] = int(post['deleted'])
                    post['last'] = int(post['last'])
                    post['locked'] = int(post['locked'])
                    post['parentid'] = int(post['parentid'])
                    post['timestamp'] = int(post['timestamp'])
                    post['boardid'] = int(post['boardid'])
                values['posts'] = posts
        elif method == 'threadlist':
            data_board = formdata.get('dir')
            data_offset = formdata.get('offset')
            data_limit = formdata.get('limit')
            data_replies = formdata.get('replies')
            offset = 0
            limit = 50
            numreplies = 2

            if not data_board:
                raise APIError, "Missing parameters"

            if data_limit:
                try:
                    limit = int(data_limit)
                except ValueError:
                    raise APIError, "Limit must be numeric"

            if data_offset:
                try:
                    offset = int(data_offset)
                except ValueError:
                    raise APIError, "Offset must be numeric"

            if data_replies:
                try:
                    numreplies = int(data_replies)
                except ValueError:
                    raise APIError, "Replies must be numeric"

            if data_replies and limit > 30:
                raise APIError, "Maximum limit is 30"

            board = setBoard(data_board)

            #sql = "SELECT id, timestamp, bumped, timestamp_formatted, name, tripcode, email, subject, message, file, thumb FROM posts WHERE boardid = %s AND parentid = 0 AND IS_DELETED = 0 ORDER BY bumped DESC LIMIT %d" % (board['id'], limit)
            sql = "SELECT INET6_NTOA(p.ip) AS ip, p.IS_DELETED AS deleted, p.id, p.timestamp, p.bumped, p.expires, p.expires_formatted, p.timestamp_formatted, p.name, p.tripcode, p.email, p.subject, p.message, p.file, p.file_size, p.image_width, p.image_height, p.thumb, p.thumb_height, p.thumb_width, p.locked, coalesce(x.count,0) AS total_replies, coalesce(x.files,0) AS total_files FROM `posts` AS p LEFT JOIN (SELECT parentid, count(1) as count, count(nullif(file, '')) as files FROM `posts` WHERE boardid = %(board)s GROUP BY parentid) AS x ON p.id=x.parentid WHERE p.parentid = 0 AND p.boardid = %(board)s ORDER BY `bumped` DESC LIMIT %(limit)d OFFSET %(offset)d" % {
                'board': board["id"], 'limit': limit, 'offset': offset}

            threads = FetchAll(sql)

            if numreplies:
                for thread in threads:
                    lastreplies = FetchAll("SELECT INET6_NTOA(ip) AS ip, id, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_height, image_width, thumb, thumb_width, thumb_height, IS_DELETED AS deleted FROM `posts` WHERE parentid = %s AND boardid = %s ORDER BY `timestamp` DESC LIMIT %d" % (
                        thread['id'], board['id'], numreplies))
                    lastreplies = lastreplies[::-1]
                    thread['id'] = int(thread['id'])
                    thread['timestamp'] = int(thread['timestamp'])
                    thread['bumped'] = int(thread['bumped'])
                    thread['expires'] = int(thread['expires'])
                    thread['total_replies'] = int(thread['total_replies'])
                    thread['total_files'] = int(thread['total_files'])
                    thread['file_size'] = int(thread['file_size'])
                    thread['image_width'] = int(thread['image_width'])
                    thread['image_height'] = int(thread['image_height'])
                    thread['thumb_width'] = int(thread['thumb_width'])
                    thread['thumb_height'] = int(thread['thumb_height'])
                    thread['locked'] = int(thread['locked'])

                    thread['replies'] = []

                    for post in lastreplies:
                        post['deleted'] = int(post['deleted'])
                        post['id'] = int(post['id'])
                        post['timestamp'] = int(post['timestamp'])

                        if post['deleted']:
                            empty_post = {'id': post['id'],
                                          'deleted': post['deleted'],
                                          'timestamp': post['timestamp'],
                                          }
                            thread['replies'].append(empty_post)
                        else:
                            post['file_size'] = int(post['file_size'])
                            post['image_width'] = int(post['image_width'])
                            post['image_height'] = int(post['image_height'])
                            post['thumb_width'] = int(post['thumb_width'])
                            post['thumb_height'] = int(post['thumb_height'])
                            post['message'] = post['message'].decode(
                                'utf-8', 'replace')

                            thread['replies'].append(post)

            values['threads'] = threads
        elif method == 'reports':
            if len(path_split) > 3:
                if path_split[3] == 'ignore':
                    report_id = formdata.get("id")
                    UpdateDb("DELETE FROM `reports` WHERE `id` = '" +
                             _mysql.escape_string(report_id)+"'")
                else:
                    values['state'] = "error"
            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")
                values['reports'] = reports
        elif method == 'logs':
            logs = FetchAll("SELECT * FROM `logs` ORDER BY `timestamp` DESC")
            values['logs'] = logs
        elif method == 'staffPosts':
            posts = FetchAll(
                "SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC")
            values['posts'] = posts
        elif method == 'login':
            values['token'] = token
        elif method == 'members':
            members = FetchAll(
                "SELECT * FROM `staff` ORDER BY lastactive DESC")
            values['members'] = members
        elif method == 'stats':
            report_count = FetchOne("SELECT COUNT(id) FROM `reports`")
            try:
                with open('stats.json', 'r') as f:
                    out = json.load(f)
                    values['stats'] = out
            except ValueError:
                values['stats'] = None
                raise APIError, "Stats error"
            values['stats']['reportCount'] = report_count['COUNT(id)']
        else:
            raise APIError, "Invalid method"

    values['time'] = int(t)
    return json.dumps(values, sort_keys=True, separators=(',', ':'))


def api_error(errtype, msg, type=None, detail=None):
    values = {'state': errtype, 'message': msg}

    if type:
        values['type'] = type
    if detail:
        values['detail'] = detail

    return json.dumps(values)


def newSession(staff_id):
    import uuid
    session_uuid = uuid.uuid4().hex

    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))

    return session_uuid


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))

    if session:
        return session

    return None


def renewSession(session_id):
    param_session_id = _mysql.escape_string(session_id)
    param_expires = timestamp() + Settings.SESSION_TIME

    UpdateDb("UPDATE `session` SET expires = %d WHERE session_id = UNHEX('%s')" %
             (param_expires, param_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)


def cleanSessions():
    param_now = timestamp()

    UpdateDb("DELETE FROM `session` WHERE expires <= %d" % param_now)


def logAction(staff, action):
    InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + str(timestamp()) +
             ", '" + _mysql.escape_string(staff) + "\', \' [API] " + _mysql.escape_string(action) + "\')")


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)
    if not staff_account:
        return None

    try:
        ph.verify(staff_account['password'], passwd)
    except argon2.exceptions.VerifyMismatchError:
        return None
    except argon2.exceptions.InvalidHash:
        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'])
        UpdateDb("UPDATE staff SET password = '%s' WHERE id = %s" %
                 (param_new_hash, staff_account['id']))

    return staff_account


class APIError(Exception):
    pass