diff --git a/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py new file mode 100644 index 0000000..152c440 --- /dev/null +++ b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py @@ -0,0 +1,30 @@ +"""Add uploader_ip column to torrents table. + +Revision ID: 3001f79b7722 +Revises: +Create Date: 2017-05-21 18:01:35.472717 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3001f79b7722' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('nyaa_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True)) + op.add_column('sukebei_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('nyaa_torrents', 'uploader_ip') + op.drop_column('sukebei_torrents', 'uploader_ip') + # ### end Alembic commands ### diff --git a/nyaa/backend.py b/nyaa/backend.py index 6be9b5d..b5853d0 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -1,3 +1,4 @@ +import flask from nyaa import app, db from nyaa import models, forms from nyaa import bencode, utils @@ -8,6 +9,7 @@ import json from werkzeug import secure_filename from collections import OrderedDict from orderedset import OrderedSet +from ipaddress import ip_address def _replace_utf8_values(dict_or_list): @@ -53,7 +55,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): description=description, encoding=torrent_encoding, filesize=torrent_filesize, - user=uploading_user) + user=uploading_user, + uploader_ip=ip_address(flask.request.remote_addr).packed) # Store bencoded info_dict torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict) diff --git a/nyaa/forms.py b/nyaa/forms.py index d7cea26..783e428 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -263,7 +263,7 @@ class UploadForm(FlaskForm): class UserForm(FlaskForm): - user_class = DisabledSelectField('Change User Class') + user_class = SelectField('Change User Class') def validate_user_class(form, field): if not field.data: @@ -294,7 +294,8 @@ def _validate_trackers(torrent_dict, tracker_to_check_for=None): for announce in announce_list: _validate_list(announce, 'announce-list item') - announce_string = _validate_bytes(announce[0], 'announce-list item url', test_decode='utf-8') + announce_string = _validate_bytes( + announce[0], 'announce-list item url', test_decode='utf-8') if tracker_to_check_for and announce_string.lower() == tracker_to_check_for.lower(): tracker_found = True diff --git a/nyaa/models.py b/nyaa/models.py index ca00e9a..07f75da 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -6,6 +6,7 @@ from sqlalchemy import func, ForeignKeyConstraint, Index from sqlalchemy_utils import ChoiceType, EmailType, PasswordType from werkzeug.security import generate_password_hash, check_password_hash from sqlalchemy_fulltext import FullText +from ipaddress import ip_address import re import base64 @@ -61,6 +62,7 @@ class Torrent(db.Model): encoding = db.Column(db.String(length=32), nullable=False) flags = db.Column(db.Integer, default=0, nullable=False, index=True) uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + uploader_ip = db.Column(db.Binary(length=16), default=None, nullable=True) has_torrent = db.Column(db.Boolean, nullable=False, default=False) created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False) @@ -92,11 +94,11 @@ class Torrent(db.Model): info = db.relationship('TorrentInfo', uselist=False, cascade="all, delete-orphan", back_populates='torrent') filelist = db.relationship('TorrentFilelist', uselist=False, - cascade="all, delete-orphan", back_populates='torrent') + cascade="all, delete-orphan", back_populates='torrent') stats = db.relationship('Statistic', uselist=False, - cascade="all, delete-orphan", back_populates='torrent', lazy='joined') + cascade="all, delete-orphan", back_populates='torrent', lazy='joined') trackers = db.relationship('TorrentTrackers', uselist=True, - cascade="all, delete-orphan", lazy='joined') + cascade="all, delete-orphan", lazy='joined') def __repr__(self): return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self) @@ -138,6 +140,11 @@ class Torrent(db.Model): def magnet_uri(self): return create_magnet(self) + @property + def uploader_ip_string(self): + if self.uploader_ip: + return str(ip_address(self.uploader_ip)) + @property def anonymous(self): return self.flags & TorrentFlags.ANONYMOUS @@ -313,7 +320,7 @@ class SubCategory(db.Model): class UserLevelType(IntEnum): REGULAR = 0 TRUSTED = 1 - ADMIN = 2 + MODERATOR = 2 SUPERADMIN = 3 @@ -362,6 +369,11 @@ class User(db.Model): ] return all(checks) + @property + def ip_string(self): + if self.last_login_ip: + return str(ip_address(self.last_login_ip)) + @classmethod def by_id(cls, id): return cls.query.get(id) @@ -381,8 +393,8 @@ class User(db.Model): return cls.by_username(username_or_email) or cls.by_email(username_or_email) @property - def is_admin(self): - return self.level >= UserLevelType.ADMIN + def is_moderator(self): + return self.level >= UserLevelType.MODERATOR @property def is_superadmin(self): diff --git a/nyaa/routes.py b/nyaa/routes.py index 23a68d2..a9652d5 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -11,7 +11,7 @@ import config import json from datetime import datetime, timedelta -import ipaddress +from ipaddress import ip_address import os.path import base64 from urllib.parse import quote @@ -135,6 +135,7 @@ def get_category_id_map(): app.register_blueprint(api_handler.api_blueprint, url_prefix='/api') + def chain_get(source, *args): ''' Tries to return values from source by the given keys. Returns None if none match. @@ -146,6 +147,7 @@ def chain_get(source, *args): return value return None + @app.route('/rss', defaults={'rss': True}) @app.route('/', defaults={'rss': False}) def home(rss): @@ -194,7 +196,7 @@ def home(rss): if flask.g.user: query_args['logged_in_user'] = flask.g.user - if flask.g.user.is_admin: # God mode + if flask.g.user.is_moderator: # God mode query_args['admin'] = True # If searching, we get results from elastic search @@ -215,7 +217,8 @@ def home(rss): if render_as_rss: return render_rss('"{}"'.format(search_term), query_results, use_elastic=True, magnet_links=use_magnet_links) else: - rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name) + rss_query_string = _generate_query_string( + search_term, category, quality_filter, user_name) max_results = min(max_search_results, query_results['hits']['total']) # change p= argument to whatever you change page_parameter to or pagination breaks pagination = Pagination(p=query_args['page'], per_page=results_per_page, @@ -238,7 +241,8 @@ def home(rss): if render_as_rss: return render_rss('Home', query, use_elastic=False, magnet_links=use_magnet_links) else: - rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name) + rss_query_string = _generate_query_string( + search_term, category, quality_filter, user_name) # Use elastic is always false here because we only hit this section # if we're browsing without a search term (which means we default to DB) # or if ES is disabled @@ -256,22 +260,23 @@ def view_user(user_name): if not user: flask.abort(404) - if flask.g.user and flask.g.user.id != user.id: - admin = flask.g.user.is_admin - superadmin = flask.g.user.is_superadmin - else: - admin = False - superadmin = False + admin_form = None + if flask.g.user and flask.g.user.is_moderator and flask.g.user.level > user.level: + admin_form = forms.UserForm() + default, admin_form.user_class.choices = _create_user_class_choices(user) + if flask.request.method == 'GET': + admin_form.user_class.data = default - form = forms.UserForm() - form.user_class.choices = _create_user_class_choices() - if flask.request.method == 'POST' and form.validate(): - selection = form.user_class.data + if flask.request.method == 'POST' and admin_form and admin_form.validate(): + selection = admin_form.user_class.data if selection == 'regular': user.level = models.UserLevelType.REGULAR elif selection == 'trusted': user.level = models.UserLevelType.TRUSTED + elif selection == 'moderator': + user.level = models.UserLevelType.MODERATOR + db.session.add(user) db.session.commit() @@ -311,7 +316,7 @@ def view_user(user_name): if flask.g.user: query_args['logged_in_user'] = flask.g.user - if flask.g.user.is_admin: # God mode + if flask.g.user.is_moderator: # God mode query_args['admin'] = True # Use elastic search for term searching @@ -344,9 +349,7 @@ def view_user(user_name): user_page=True, rss_filter=rss_query_string, level=user_level, - admin=admin, - superadmin=superadmin, - form=form) + admin_form=admin_form) # Similar logic as home page else: if use_elastic: @@ -362,9 +365,7 @@ def view_user(user_name): user_page=True, rss_filter=rss_query_string, level=user_level, - admin=admin, - superadmin=superadmin, - form=form) + admin_form=admin_form) @app.template_filter('rfc822') @@ -417,7 +418,7 @@ def login(): return flask.redirect(flask.url_for('login')) user.last_login_date = datetime.utcnow() - user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed + user.last_login_ip = ip_address(flask.request.remote_addr).packed db.session.add(user) db.session.commit() @@ -451,7 +452,7 @@ def register(): if flask.request.method == 'POST' and form.validate(): user = models.User(username=form.username.data.strip(), email=form.email.data.strip(), password=form.password.data) - user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed + user.last_login_ip = ip_address(flask.request.remote_addr).packed db.session.add(user) db.session.commit() @@ -479,13 +480,7 @@ def profile(): form = forms.ProfileForm(flask.request.form) - level = 'Regular' - if flask.g.user.is_admin: - level = 'Moderator' - if flask.g.user.is_superadmin: # check this second because we can be admin AND superadmin - level = 'Administrator' - elif flask.g.user.is_trusted: - level = 'Trusted' + level = ['Regular', 'Trusted', 'Moderator', 'Administrator'][flask.g.user.level] if flask.request.method == 'POST' and form.validate(): user = flask.g.user @@ -586,11 +581,11 @@ def view_torrent(torrent_id): flask.abort(404) # Only allow admins see deleted torrents - if torrent.deleted and not (viewer and viewer.is_admin): + if torrent.deleted and not (viewer and viewer.is_moderator): flask.abort(404) # Only allow owners and admins to edit torrents - can_edit = viewer and (viewer is torrent.user or viewer.is_admin) + can_edit = viewer and (viewer is torrent.user or viewer.is_moderator) files = None if torrent.filelist: @@ -614,11 +609,11 @@ def edit_torrent(torrent_id): flask.abort(404) # Only allow admins edit deleted torrents - if torrent.deleted and not (editor and editor.is_admin): + if torrent.deleted and not (editor and editor.is_moderator): flask.abort(404) # Only allow torrent owners or admins edit torrents - if not editor or not (editor is torrent.user or editor.is_admin): + if not editor or not (editor is torrent.user or editor.is_moderator): flask.abort(403) if flask.request.method == 'POST' and form.validate(): @@ -636,7 +631,7 @@ def edit_torrent(torrent_id): if editor.is_trusted: torrent.trusted = form.is_trusted.data - if editor.is_admin: + if editor.is_moderator: torrent.deleted = form.is_deleted.data db.session.commit() @@ -739,11 +734,22 @@ def send_verification_email(to_address, activ_link): server.quit() -def _create_user_class_choices(): +def _create_user_class_choices(user): choices = [('regular', 'Regular')] - if flask.g.user and flask.g.user.is_superadmin: - choices.append(('trusted', 'Trusted')) - return choices + default = 'regular' + if flask.g.user: + if flask.g.user.is_moderator: + choices.append(('trusted', 'Trusted')) + if flask.g.user.is_superadmin: + choices.append(('moderator', 'Moderator')) + + if user: + if user.is_moderator: + default = 'moderator' + elif user.is_trusted: + default = 'trusted' + + return default, choices # #################################### STATIC PAGES #################################### diff --git a/nyaa/templates/edit.html b/nyaa/templates/edit.html index 44dfd51..b65ce0c 100644 --- a/nyaa/templates/edit.html +++ b/nyaa/templates/edit.html @@ -31,7 +31,7 @@