diff --git a/docker-compose-operation.yml b/docker-compose-operation.yml index 09ca045cc..02ddedfea 100644 --- a/docker-compose-operation.yml +++ b/docker-compose-operation.yml @@ -1,3 +1,5 @@ +version: '2.3' + services: files: container_name: "themotte" diff --git a/files/__main__.py b/files/__main__.py index 52d1a0ce2..78351a36e 100644 --- a/files/__main__.py +++ b/files/__main__.py @@ -28,7 +28,7 @@ app.jinja_env.cache = {} app.jinja_env.auto_reload = True faulthandler.enable() -if bool_from_string(environ.get("ENFORCE_PRODUCTION", True)) and app.config["DEBUG"]: +if bool_from_string(environ.get("ENFORCE_PRODUCTION", True)) and app.debug: raise ValueError("Debug mode is not allowed! If this is a dev environment, please set ENFORCE_PRODUCTION to false") if environ.get("SITE_ID") is None: @@ -176,6 +176,7 @@ def before_request(): return 'Please use a "User-Agent" header!', 403 ua = g.agent.lower() + g.debug = app.debug g.webview = ('; wv) ' in ua) g.inferior_browser = ( 'iphone' in ua or diff --git a/files/helpers/const.py b/files/helpers/const.py index 4d7b96e07..a2bd09702 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -75,6 +75,10 @@ VIDEO_FORMATS = ['mp4','webm','mov','avi','mkv','flv','m4v','3gp'] AUDIO_FORMATS = ['mp3','wav','ogg','aac','m4a','flac'] NO_TITLE_EXTENSIONS = IMAGE_FORMATS + VIDEO_FORMATS + AUDIO_FORMATS +PERMS = { + "DEBUG_LOGIN_TO_OTHERS": 3, +} + AWARDS = { "lootbox": { "kind": "lootbox", @@ -124,14 +128,6 @@ AWARDS = { "color": "text-yellow", "price": 300 }, - "tilt": { - "kind": "tilt", - "title": "Tilt", - "description": "Tilts the post or comment", - "icon": "fas fa-car-tilt", - "color": "text-blue", - "price": 300 - }, "glowie": { "kind": "glowie", "title": "Glowie", diff --git a/files/helpers/get.py b/files/helpers/get.py index 886b31677..8bfcb48a1 100644 --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -2,24 +2,22 @@ from collections import defaultdict from typing import Iterable, List, Optional, Type, Union from flask import g -from sqlalchemy import and_, any_, or_ +from sqlalchemy import and_, or_, func from sqlalchemy.orm import selectinload from files.classes import * from files.helpers.const import AUTOJANNY_ID from files.helpers.contentsorting import sort_comment_results -from files.helpers.strings import sql_ilike_clean def get_id( username:str, graceful:bool=False) -> Optional[int]: - username = sql_ilike_clean(username) user = g.db.query(User.id).filter( or_( - User.username.ilike(username), - User.original_username.ilike(username) + func.lower(User.username) == username.lower(), + func.lower(User.original_username) == username.lower() ) ).one_or_none() @@ -35,15 +33,14 @@ def get_user( v:Optional[User]=None, graceful:bool=False, include_blocks:bool=False) -> Optional[User]: - username = sql_ilike_clean(username) if not username: if graceful: return None abort(404) user = g.db.query(User).filter( or_( - User.username.ilike(username), - User.original_username.ilike(username) + func.lower(User.username) == username.lower(), + func.lower(User.original_username) == username.lower() ) ).one_or_none() @@ -61,14 +58,13 @@ def get_users( usernames:Iterable[str], graceful:bool=False) -> List[User]: if not usernames: return [] - usernames = [ sql_ilike_clean(n) for n in usernames ] if not any(usernames): if graceful and len(usernames) == 0: return [] abort(404) users = g.db.query(User).filter( or_( - User.username == any_(usernames), - User.original_username == any_(usernames) + func.lower(User.username).in_([name.lower() for name in usernames]), + func.lower(User.original_username).in_([name.lower() for name in usernames]) ) ).all() diff --git a/files/helpers/jinja2.py b/files/helpers/jinja2.py index 90702f09c..e1b61051c 100644 --- a/files/helpers/jinja2.py +++ b/files/helpers/jinja2.py @@ -65,6 +65,7 @@ def inject_constants(): "DEFAULT_COLOR":DEFAULT_COLOR, "COLORS":COLORS, "THEMES":THEMES, + "PERMS":PERMS, } def template_function(func): diff --git a/files/routes/__init__.py b/files/routes/__init__.py index f6379d6bb..3af165546 100644 --- a/files/routes/__init__.py +++ b/files/routes/__init__.py @@ -1,3 +1,5 @@ +from files.__main__ import app + from .admin import * from .comments import * from .errors import * @@ -15,4 +17,6 @@ from .feeds import * from .awards import * from .giphy import * from .volunteer import * +if app.debug: + from .dev import * # from .subs import * diff --git a/files/routes/awards.py b/files/routes/awards.py index 82b08d885..fd4c3d9c8 100644 --- a/files/routes/awards.py +++ b/files/routes/awards.py @@ -336,7 +336,7 @@ def admin_userawards_post(v): try: u = request.values.get("username").strip() except: abort(404) - whitelist = ("shit", "fireflies", "train", "scooter", "wholesome", "tilt", "glowie") + whitelist = ("shit", "fireflies", "train", "scooter", "wholesome", "glowie") u = get_user(u, graceful=False, v=v) diff --git a/files/routes/dev.py b/files/routes/dev.py new file mode 100644 index 000000000..eeeee4451 --- /dev/null +++ b/files/routes/dev.py @@ -0,0 +1,20 @@ +from secrets import token_hex +from flask import session, redirect, request + +from files.helpers.const import PERMS +from files.helpers.get import get_user +from files.helpers.wrappers import admin_level_required +from files.__main__ import app + +if not app.debug: + raise ImportError("Importing dev routes is not allowed outside of debug mode!") + +@app.post('/dev/sessions/') +@admin_level_required(PERMS['DEBUG_LOGIN_TO_OTHERS']) +def login_to_other_account(v): + u = get_user(request.values.get('username')) + session.permanent = True + session["lo_user"] = u.id + session["login_nonce"] = u.login_nonce + session["session_id"] = token_hex(49) + return redirect('/') diff --git a/files/routes/front.py b/files/routes/front.py index 57632a66a..1bdc3393e 100644 --- a/files/routes/front.py +++ b/files/routes/front.py @@ -1,5 +1,6 @@ from files.helpers.wrappers import * from files.helpers.get import * +from files.helpers.strings import sql_ilike_clean from files.__main__ import app, cache, limiter from files.classes.submission import Submission from files.helpers.contentsorting import apply_time_filter, sort_objects @@ -269,7 +270,7 @@ def frontlist(v=None, sort='new', page=1, t="all", ids_only=True, ccmode="false" if v and filter_words: for word in filter_words: - word = word.replace(r'\\', '').replace('_', r'\_').replace('%', r'\%').strip() + word = sql_ilike_clean(word).strip() posts=posts.filter(not_(Submission.title.ilike(f'%{word}%'))) if not (v and v.shadowbanned): diff --git a/files/routes/login.py b/files/routes/login.py index b32412cd3..20d5b08a9 100644 --- a/files/routes/login.py +++ b/files/routes/login.py @@ -2,7 +2,6 @@ from urllib.parse import urlencode from files.mail import * from files.__main__ import app, limiter from files.helpers.const import * -from files.helpers.strings import sql_ilike_clean import requests @app.get("/login") @@ -90,7 +89,7 @@ def login_post(): if username.startswith('@'): username = username[1:] if "@" in username: - try: account = g.db.query(User).filter(User.email.ilike(sql_ilike_clean(username))).one_or_none() + try: account = g.db.query(User).filter(func.lower(User.email) == username.lower()).one_or_none() except: return "Multiple users use this email!" else: account = get_user(username, graceful=True) @@ -189,8 +188,7 @@ def sign_up_get(v): ref = request.values.get("ref") if ref: - ref = sql_ilike_clean(ref) - ref_user = g.db.query(User).filter(User.username.ilike(ref)).one_or_none() + ref_user = g.db.query(User).filter(func.lower(User.username) == ref.lower()).one_or_none() else: ref_user = None @@ -386,13 +384,11 @@ def post_forgot(): if not email_regex.fullmatch(email): return render_template("forgot_password.html", error="Invalid email.") - - username = sql_ilike_clean(username.lstrip('@')) - email = sql_ilike_clean(email) + username = username.lstrip('@') user = g.db.query(User).filter( - User.username.ilike(username), - User.email.ilike(email)).one_or_none() + func.lower(User.username) == username.lower(), + func.lower(User.email) == email.lower()).one_or_none() if user: now = int(time.time()) diff --git a/files/routes/posts.py b/files/routes/posts.py index 5b9dc6b16..4d6f8bb8c 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -2,10 +2,10 @@ import time import gevent from files.helpers.wrappers import * from files.helpers.sanitize import * -from files.helpers.strings import sql_ilike_clean from files.helpers.alerts import * from files.helpers.contentsorting import sort_objects from files.helpers.const import * +from files.helpers.strings import sql_ilike_clean from files.classes import * from flask import * from io import BytesIO @@ -658,7 +658,7 @@ def api_is_repost(): if url.endswith('/'): url = url[:-1] - search_url = url.replace('%', '').replace(r'\\', '').replace('_', r'\_').strip() + search_url = sql_ilike_clean(url) repost = g.db.query(Submission).filter( Submission.url.ilike(search_url), Submission.deleted_utc == 0, @@ -735,13 +735,12 @@ def submit_post(v, sub=None): query=urlencode(filtered, doseq=True), fragment=parsed_url.fragment) - url = urlunparse(new_url) + search_url = urlunparse(new_url) - if url.endswith('/'): url = url[:-1] + if search_url.endswith('/'): url = url[:-1] - search_url = sql_ilike_clean(url) repost = g.db.query(Submission).filter( - Submission.url.ilike(search_url), + func.lower(Submission.url) == search_url.lower(), Submission.deleted_utc == 0, Submission.is_banned == False ).first() diff --git a/files/routes/search.py b/files/routes/search.py index c6daecccb..da436f388 100644 --- a/files/routes/search.py +++ b/files/routes/search.py @@ -72,8 +72,7 @@ def searchposts(v): else: posts = posts.filter(Submission.author_id == author.id) if 'q' in criteria: - words=criteria['q'].split() - words = criteria['q'].replace(r'\\', '').replace('_', r'\_').replace('%', r'\%').strip().split() + words = sql_ilike_clean(criteria['q']).split() words=[Submission.title.ilike('%'+x+'%') for x in words] posts=posts.filter(*words) @@ -158,7 +157,7 @@ def searchcomments(v): else: comments = comments.filter(Comment.author_id == author.id) if 'q' in criteria: - words = criteria['q'].replace(r'\\', '').replace('_', r'\_').replace('%', r'\%').strip().split() + words = sql_ilike_clean(criteria['q']).split() words = [Comment.body.ilike('%'+x+'%') for x in words] comments = comments.filter(*words) diff --git a/files/routes/settings.py b/files/routes/settings.py index c05d9a07e..9acb82fe8 100644 --- a/files/routes/settings.py +++ b/files/routes/settings.py @@ -626,12 +626,10 @@ def settings_name_change(v): v=v, error="This isn't a valid username.") - search_name = sql_ilike_clean(new_name) - - x= g.db.query(User).filter( + x = g.db.query(User).filter( or_( - User.username.ilike(search_name), - User.original_username.ilike(search_name) + func.lower(User.username) == new_name.lower(), + func.lower(User.original_username) == new_name.lower() ) ).one_or_none() diff --git a/files/routes/volunteer.py b/files/routes/volunteer.py index 736ed268d..b78973a91 100644 --- a/files/routes/volunteer.py +++ b/files/routes/volunteer.py @@ -6,7 +6,7 @@ import files.helpers.jinja2 from files.helpers.wrappers import auth_required from files.routes.volunteer_common import VolunteerDuty import files.routes.volunteer_janitor -from flask import render_template, g, request +from flask import abort, render_template, g, request from os import environ import sqlalchemy from typing import Optional diff --git a/files/routes/volunteer_janitor.py b/files/routes/volunteer_janitor.py index ec663fcd6..707610d4e 100644 --- a/files/routes/volunteer_janitor.py +++ b/files/routes/volunteer_janitor.py @@ -1,4 +1,5 @@ +from typing import Optional from files.__main__ import app from files.classes.comment import Comment from files.classes.flags import CommentFlag @@ -33,7 +34,7 @@ class VolunteerDutyJanitor(VolunteerDuty): return g.db.query(Comment).where(Comment.id.in_(self.choices)) -def get_duty(u: User) -> VolunteerDutyJanitor: +def get_duty(u: User) -> Optional[VolunteerDutyJanitor]: if not app.config['VOLUNTEER_JANITOR_ENABLE']: return None diff --git a/files/templates/admin/admin_home.html b/files/templates/admin/admin_home.html index 4639be72a..46957bcd0 100644 --- a/files/templates/admin/admin_home.html +++ b/files/templates/admin/admin_home.html @@ -59,7 +59,7 @@
  • Daily Stat Chart
  • -{% if v.admin_level > 2 %} +{% if v.admin_level >= 3 %}
    
     	
    @@ -106,5 +106,20 @@ {% endif %} - +{% if g.debug %} +
    +

    Debug Options

    + {% if v.admin_level >= PERMS["DEBUG_LOGIN_TO_OTHERS"] %} + + {% endif %} +
    +{% endif %} {% endblock %} diff --git a/files/templates/comments.html b/files/templates/comments.html index 92e756ba2..ed45dd5c9 100644 --- a/files/templates/comments.html +++ b/files/templates/comments.html @@ -253,32 +253,6 @@ {% if c.is_banned and c.ban_reason %}
    removed by @{{c.ban_reason}}
    {% endif %} - - {% if c.award_count("tilt") %} - - {% endif %}
    {{c.realbody(v) | safe}} diff --git a/supervisord.conf.release b/supervisord.conf.release index ef071c9b1..2ebb0138a 100644 --- a/supervisord.conf.release +++ b/supervisord.conf.release @@ -5,7 +5,7 @@ logfile=/tmp/supervisord.log [program:service] directory=/service -command=sh -c 'python3 -m flask db upgrade && ENABLE_SERVICES=true gunicorn files.__main__:app -k gevent -w 1 --reload -b 0.0.0.0:80 --max-requests 1000 --max-requests-jitter 500' +command=sh -c 'python3 -m flask db upgrade && ENABLE_SERVICES=true gunicorn files.__main__:app -k gevent -w $(( `nproc` * 2 )) --reload -b 0.0.0.0:80 --max-requests 1000 --max-requests-jitter 500' stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr