Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

α → β ⑬ #471

Merged
merged 43 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f086be5
Quoted and negated search terms, up front search help
HebaruSan Nov 13, 2021
edf126f
Scan uploads with pyclamd
HebaruSan Nov 11, 2021
d89e449
Bump shell-quote from 1.7.2 to 1.7.3 in /frontend
dependabot[bot] Jun 23, 2022
93871a8
Merge pull request #453 from KSP-SpaceDock/dependabot/npm_and_yarn/fr…
HebaruSan Jul 1, 2022
c27f273
Bump moment from 2.29.1 to 2.29.4 in /frontend (#454)
dependabot[bot] Jul 8, 2022
3f1f580
Use mod version entered by user unaltered
HebaruSan Jul 8, 2022
c44ca3e
Bump json-schema from 0.2.3 to 0.4.0 in /frontend (#457)
dependabot[bot] Sep 2, 2022
21d1b4e
Modifieed piwik code to reflect switch to matomo
V1TA5 Sep 4, 2022
edc33d8
Modified tracking js code to reflect switch from wiwik to matomo
V1TA5 Sep 4, 2022
de38106
Bump minimatch from 3.0.4 to 3.0.8 in /frontend (#458)
dependabot[bot] Nov 25, 2022
9cf0dcd
Merge pull request #455 from HebaruSan/fix/version-secure
V1TA5 Feb 7, 2023
41a124c
Merge pull request #432 from HebaruSan/feature/search-quotes-and-nega…
V1TA5 Feb 7, 2023
80edb5f
Merge pull request #429 from HebaruSan/feature/antivirus
V1TA5 Feb 7, 2023
871c4a0
Add game ID filter
RedstoneWizard08 Mar 2, 2023
f2d9d6b
Add for /api/browse/new
RedstoneWizard08 Mar 2, 2023
828ba54
Add version filter
RedstoneWizard08 Mar 2, 2023
c8635fe
Fix query not being affected by order
RedstoneWizard08 Mar 2, 2023
766a749
Merge pull request #466 from RedstoneWizard08/feature/search-filters
V1TA5 Mar 2, 2023
1b51757
Add browse filters to api doc, filter by friendly game version (#467)
HebaruSan Mar 2, 2023
b274d20
Fix game_version_id filter
HebaruSan Mar 2, 2023
e81be0e
Check user profile field lengths
HebaruSan Mar 3, 2023
6656065
Merge pull request #470 from HebaruSan/fix/profile-desc-len
V1TA5 Mar 3, 2023
48aaf6b
Clean up login/register layout (#421)
HebaruSan Mar 3, 2023
9b8eafe
Admin pagination usability fixes (#423)
HebaruSan Mar 3, 2023
9bf0d35
Co-authorship fixes (#425)
HebaruSan Mar 3, 2023
94820ce
Widescreen layout for mod page (#426)
HebaruSan Mar 3, 2023
51cfdfa
Fix mobile hamburger menu and search (#427)
HebaruSan Mar 3, 2023
c537073
Paginated search buttons (#431)
HebaruSan Mar 3, 2023
8c46489
Send download size, co-authors, and authors' GitHub and forum names t…
HebaruSan Mar 3, 2023
7a5bd33
Parse @username links and colon emoji codes in markdown (#441)
HebaruSan Mar 3, 2023
861c4ce
API route for auto-generated remote version files (#451)
HebaruSan Mar 3, 2023
538e016
Allow mod authors to replace downloads (#428)
HebaruSan Mar 3, 2023
5c7219b
Bump scss-tokenizer and node-sass in /frontend (#462)
dependabot[bot] Mar 3, 2023
179ed0a
Deferred loading and pre-rendering for mod changelogs (#414)
HebaruSan Mar 3, 2023
95fb5fb
Fix edit_version's broken Dropzone
HebaruSan Mar 3, 2023
b529f43
Add missing quotes
HebaruSan Mar 3, 2023
5c25b69
Fix downloads performance (#416)
HebaruSan Mar 3, 2023
a36e0c0
Fix modpack backgrounds and thumbnails (#422)
HebaruSan Mar 3, 2023
dcb9168
Add Locked Mods tab to admin pages (#419)
HebaruSan Mar 3, 2023
65ad7c2
Non-public draft blog posts (#418)
HebaruSan Mar 3, 2023
3d48ded
HTML emails (#400)
HebaruSan Mar 3, 2023
356a4a7
Load editor before mods script
HebaruSan Mar 3, 2023
9dd101e
Option to profile everything, save data only for slow responses (#430)
HebaruSan Mar 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions KerbalStuff/antivirus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
from pathlib import Path
from shutil import move
from typing import Optional

import pyclamd

from .config import _cfg, _cfgi, site_logger
from .objects import User
from .email import send_mod_locked
from .ckan import notify_ckan

clam_daemon = None


def file_contains_malware(where: str) -> bool:
global clam_daemon
try:
if not clam_daemon:
clam_daemon = pyclamd.ClamdNetworkSocket(host=_cfg('clamav-host'), port=_cfgi('clamav-port', 3310))
result = clam_daemon.scan_file(where)
if result:
site_logger.error(f'ClamAV says {where} contains malware')
return True
except Exception as exc:
# No ClamAV daemon found, log it and let the file through
site_logger.error(f'Failed to connect to ClamAV, skipping scan of {where}', exc_info=exc)
return False


def quarantine_malware(path: str) -> None:
quarantine_folder = _cfg('clamav-quarantine-path')
if quarantine_folder:
move(path, Path(quarantine_folder) / Path(path).name)
else:
os.remove(path)


def punish_malware(user: User) -> None:
# Lock all of this author's mods
for other_mod in user.mods:
if not other_mod.locked:
other_mod.locked = True
other_mod.published = False
other_mod.locked_by = None
other_mod.lock_reason = 'Malware detected in upload'
send_mod_locked(other_mod, user)
notify_ckan(other_mod, 'locked', True)
40 changes: 22 additions & 18 deletions KerbalStuff/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
import xml.etree.ElementTree as ET
from time import strftime
from datetime import timedelta
from typing import Tuple, List, Dict, Any, Optional, Union
from pathlib import Path

Expand All @@ -14,16 +15,16 @@
from flask import Flask, render_template, g, url_for, Response, request
from flask_login import LoginManager, current_user
from flaskext.markdown import Markdown
from sqlalchemy import desc
from werkzeug.exceptions import HTTPException, InternalServerError, NotFound
from flask.typing import ResponseReturnValue
from jinja2 import ChainableUndefined
from pymdownx.emoji import gemoji, to_alt

from .blueprints.accounts import accounts
from .blueprints.admin import admin
from .blueprints.anonymous import anonymous
from .blueprints.api import api
from .blueprints.blog import blog
from .blueprints.blog import blog, get_all_announcement_posts, get_non_member_announcement_posts
from .blueprints.lists import lists
from .blueprints.login_oauth import list_defined_oauths, login_oauth
from .blueprints.mods import mods
Expand Down Expand Up @@ -60,16 +61,29 @@
app.secret_key = _cfg("secret-key")
app.json_encoder = CustomJSONEncoder
app.session_interface = OnlyLoggedInSessionInterface()
Markdown(app, extensions=[KerbDown(), 'fenced_code'])
Markdown(app, extensions=[KerbDown(), 'fenced_code', 'pymdownx.emoji'],
extension_configs={'pymdownx.emoji': {
# GitHub's emojis
'emoji_index': gemoji,
# Unicode output
'emoji_generator': to_alt
}})
login_manager = LoginManager(app)

prof_dir = _cfg('profile-dir')
if prof_dir:
from .middleware.profiler import ConditionalProfilerMiddleware
from .profiling import sampling_function
Path(prof_dir).mkdir(parents=True, exist_ok=True)
app.wsgi_app = ConditionalProfilerMiddleware( # type: ignore[assignment]
app.wsgi_app, stream=None, profile_dir=prof_dir, sampling_function=sampling_function)
log_if_longer = _cfg('profile-threshold-ms')
if log_if_longer:
from .middleware.profiler import CherrypickingProfilerMiddleware
Path(prof_dir).mkdir(parents=True, exist_ok=True)
app.wsgi_app = CherrypickingProfilerMiddleware( # type: ignore[assignment]
app.wsgi_app, stream=None, profile_dir=prof_dir, log_if_longer=timedelta(milliseconds=int(log_if_longer)))
else:
from .middleware.profiler import ConditionalProfilerMiddleware
from .profiling import sampling_function
Path(prof_dir).mkdir(parents=True, exist_ok=True)
app.wsgi_app = ConditionalProfilerMiddleware( # type: ignore[assignment]
app.wsgi_app, stream=None, profile_dir=prof_dir, sampling_function=sampling_function)


@login_manager.user_loader
Expand Down Expand Up @@ -321,13 +335,3 @@ def inject() -> Dict[str, Any]:
'donation_header_link': _cfgb('donation-header-link') if not dismissed_donation else False,
'registration': _cfgb('registration')
}


def get_all_announcement_posts() -> List[BlogPost]:
return BlogPost.query.filter(BlogPost.announcement).order_by(desc(BlogPost.created)).all()


def get_non_member_announcement_posts() -> List[BlogPost]:
return BlogPost.query.filter(
BlogPost.announcement, BlogPost.members_only != True
).order_by(desc(BlogPost.created)).all()
40 changes: 29 additions & 11 deletions KerbalStuff/blueprints/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import math
from typing import Union, List, Tuple, Dict, Any
from typing import Union, List, Tuple, Dict, Any, Optional
import datetime
from datetime import timezone
from pathlib import Path
from subprocess import run, PIPE

from flask import Blueprint, render_template, redirect, request, abort, url_for
from flask_login import login_user, current_user
from sqlalchemy import desc, or_, func
from sqlalchemy import or_, func
from sqlalchemy.orm import Query
import werkzeug.wrappers

Expand All @@ -19,6 +19,7 @@

admin = Blueprint('admin', __name__)
ITEMS_PER_PAGE = 10
MODS_PER_PAGE = 30


@admin.route("/admin")
Expand Down Expand Up @@ -121,7 +122,7 @@ def users(page: int) -> Union[str, werkzeug.wrappers.Response]:
users = search_users(query.lower()) if query else User.query
if not show_non_public:
users = users.filter(User.public)
users = users.order_by(desc(User.created))
users = users.order_by(User.created.desc())
user_count = users.count()
# We can limit here because SqlAlchemy executes queries lazily.
users = users.offset((page - 1) * ITEMS_PER_PAGE).limit(ITEMS_PER_PAGE)
Expand All @@ -135,6 +136,23 @@ def users(page: int) -> Union[str, werkzeug.wrappers.Response]:
query=query, show_non_public=show_non_public)


@admin.route("/admin/locked_mods/<int:page>")
@adminrequired
def locked_mods(page: int) -> Union[str, werkzeug.wrappers.Response]:
if page < 1:
return redirect(url_for('admin.locked_mods', page=1, **request.args))
locked_mods = Mod.query.filter(Mod.locked == True)\
.order_by(Mod.updated.desc())
locked_mods_count = locked_mods.count()
total_pages = max(1, math.ceil(locked_mods_count / MODS_PER_PAGE))
if page > total_pages:
return redirect(url_for('admin.locked_mods', page=total_pages, **request.args))
return render_template("admin-locked-mods.html",
page=page, total_pages=total_pages,
locked_mods=locked_mods.offset((page - 1) * MODS_PER_PAGE)\
.limit(MODS_PER_PAGE))


@admin.route("/admin/blog")
@adminrequired
def blog() -> str:
Expand All @@ -143,15 +161,15 @@ def blog() -> str:

@admin.route("/admin/publishers/<int:page>")
@adminrequired
def publishers(page: int, error: str = None) -> Union[str, werkzeug.wrappers.Response]:
def publishers(page: int, error: Optional[str] = None) -> Union[str, werkzeug.wrappers.Response]:
if page < 1:
return redirect(url_for('admin.publishers', page=1, **request.args))
show_none_active = (request.args.get('show_none_active', '').lower() in TRUE_STR)
query = request.args.get('query', type=str)
publishers = search_publishers(query.lower()) if query else Publisher.query
if not show_none_active:
publishers = publishers.join(Publisher.games).filter(Game.active).distinct(Publisher.id)
publishers = publishers.order_by(desc(Publisher.id))
publishers = publishers.order_by(Publisher.id.desc())
publisher_count = publishers.count()
publishers = publishers.offset((page - 1) * ITEMS_PER_PAGE).limit(ITEMS_PER_PAGE)

Expand All @@ -167,23 +185,23 @@ def publishers(page: int, error: str = None) -> Union[str, werkzeug.wrappers.Res

@admin.route("/admin/games/<int:page>")
@adminrequired
def games(page: int, error: str = None) -> Union[str, werkzeug.wrappers.Response]:
def games(page: int, error: Optional[str] = None) -> Union[str, werkzeug.wrappers.Response]:
if page < 1:
return redirect(url_for('admin.games', page=1, **request.args))
show_inactive = (request.args.get('show_inactive', '').lower() in TRUE_STR)
query = request.args.get('query', type=str)
games = search_games(query.lower()) if query else Game.query
if not show_inactive:
games = games.filter(Game.active)
games = games.order_by(desc(Game.id))
games = games.order_by(Game.id.desc())
game_count = games.count()
games = games.offset((page - 1) * ITEMS_PER_PAGE).limit(ITEMS_PER_PAGE)

total_pages = max(1, math.ceil(game_count / ITEMS_PER_PAGE))
if page > total_pages:
return redirect(url_for('admin.games', page=total_pages, **request.args))

publishers = Publisher.query.order_by(desc(Publisher.id))
publishers = Publisher.query.order_by(Publisher.id.desc())

return render_template('admin-games.html',
games=games, publishers=publishers, game_count=game_count,
Expand All @@ -194,23 +212,23 @@ def games(page: int, error: str = None) -> Union[str, werkzeug.wrappers.Response

@admin.route("/admin/gameversions/<int:page>")
@adminrequired
def game_versions(page: int, error: str = None) -> Union[str, werkzeug.wrappers.Response]:
def game_versions(page: int, error: Optional[str] = None) -> Union[str, werkzeug.wrappers.Response]:
if page < 1:
return redirect(url_for('admin.game_versions', page=1, **request.args))
show_inactive = (request.args.get('show_inactive', '').lower() in TRUE_STR)
query = request.args.get('query', type=str)
game_versions = search_game_versions(query.lower()) if query else GameVersion.query
if not show_inactive:
game_versions = game_versions.join(GameVersion.game).filter(Game.active)
game_versions = game_versions.order_by(desc(GameVersion.id))
game_versions = game_versions.order_by(GameVersion.id.desc())
game_version_count = game_versions.count()
game_versions = game_versions.offset((page - 1) * ITEMS_PER_PAGE).limit(ITEMS_PER_PAGE)

total_pages = max(1, math.ceil(game_version_count / ITEMS_PER_PAGE))
if page > total_pages:
return redirect(url_for('admin.game_versions', page=total_pages, **request.args))

games = Game.query.order_by(desc(Game.id))
games = Game.query.order_by(Game.id.desc())

return render_template('admin-game-versions.html',
game_versions=game_versions, games=games,
Expand Down
13 changes: 6 additions & 7 deletions KerbalStuff/blueprints/anonymous.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import werkzeug.wrappers
from flask import Blueprint, render_template, abort, request, Response, make_response, send_file
from flask_login import current_user
from sqlalchemy import desc
from datetime import timezone

from ..common import dumb_object, paginate_query, get_paginated_mods, get_game_info, get_games, \
Expand Down Expand Up @@ -61,7 +60,7 @@ def browse() -> str:

@anonymous.route("/browse/new")
def browse_new() -> str:
mods = Mod.query.filter(Mod.published).order_by(desc(Mod.created))
mods = Mod.query.filter(Mod.published).order_by(Mod.created.desc())
mods, page, total_pages = paginate_query(mods)
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages,
url="/browse/new", name="Newest Mods", rss="/browse/new.rss")
Expand All @@ -80,7 +79,7 @@ def browse_new_rss() -> Response:

@anonymous.route("/browse/updated")
def browse_updated() -> str:
mods = Mod.query.filter(Mod.published, Mod.versions.any(ModVersion.id != Mod.default_version_id)).order_by(desc(Mod.updated))
mods = Mod.query.filter(Mod.published, Mod.versions.any(ModVersion.id != Mod.default_version_id)).order_by(Mod.updated.desc())
mods, page, total_pages = paginate_query(mods)
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages,
url="/browse/updated", name="Recently Updated Mods", rss="/browse/updated.rss")
Expand All @@ -107,7 +106,7 @@ def browse_top() -> str:

@anonymous.route("/browse/featured")
def browse_featured() -> str:
mods = Featured.query.order_by(desc(Featured.created))
mods = Featured.query.order_by(Featured.created.desc())
mods, page, total_pages = paginate_query(mods)
mods = [f.mod for f in mods]
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages,
Expand Down Expand Up @@ -150,7 +149,7 @@ def singlegame_browse(gameshort: str) -> str:
@anonymous.route("/<gameshort>/browse/new")
def singlegame_browse_new(gameshort: str) -> str:
ga = get_game_info(short=gameshort)
mods = Mod.query.filter(Mod.published, Mod.game_id == ga.id).order_by(desc(Mod.created))
mods = Mod.query.filter(Mod.published, Mod.game_id == ga.id).order_by(Mod.created.desc())
mods, page, total_pages = paginate_query(mods)
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages, ga=ga,
url="/browse/new", name="Newest Mods", rss="/browse/new.rss")
Expand All @@ -172,7 +171,7 @@ def singlegame_browse_new_rss(gameshort: str) -> Response:
@anonymous.route("/<gameshort>/browse/updated")
def singlegame_browse_updated(gameshort: str) -> str:
ga = get_game_info(short=gameshort)
mods = Mod.query.filter(Mod.published, Mod.game_id == ga.id, Mod.versions.any(ModVersion.id != Mod.default_version_id)).order_by(desc(Mod.updated))
mods = Mod.query.filter(Mod.published, Mod.game_id == ga.id, Mod.versions.any(ModVersion.id != Mod.default_version_id)).order_by(Mod.updated.desc())
mods, page, total_pages = paginate_query(mods)
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages, ga=ga,
url="/browse/updated", name="Recently Updated Mods", rss="/browse/updated.rss")
Expand Down Expand Up @@ -204,7 +203,7 @@ def singlegame_browse_top(gameshort: str) -> str:
def singlegame_browse_featured(gameshort: str) -> str:
ga = get_game_info(short=gameshort)
mods = Featured.query.outerjoin(Mod).filter(
Mod.game_id == ga.id).order_by(desc(Featured.created))
Mod.game_id == ga.id).order_by(Featured.created.desc())
mods, page, total_pages = paginate_query(mods)
mods = [f.mod for f in mods]
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages, ga=ga,
Expand Down
Loading