From 702abaf947b4dfd62a9971e9c6ca73f486b631db Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Sun, 1 Nov 2026 10:02:33 -0500 Subject: [PATCH 1/2] Add functionality to edit and delete user comments --- app/controllers/comment.py | 123 +++++++++++++++++++++++++++- app/models/comment.py | 80 +++++++++++++++++- templates/crackme/read.html | 59 ++++++++++++- tests/test_comments_and_crackmes.py | 103 +++++++++++++++++++++++ tests/test_models_extended.py | 17 ++++ 5 files changed, 376 insertions(+), 6 deletions(-) diff --git a/app/controllers/comment.py b/app/controllers/comment.py index 0e93576..cc0fafa 100644 --- a/app/controllers/comment.py +++ b/app/controllers/comment.py @@ -7,10 +7,14 @@ from html import escape as html_escape from flask import Blueprint, request, redirect, flash, session, render_template import bleach -from app.models.comment import comment_create, comment_by_id, comment_set_spoiler, get_thread_participants +from app.models.comment import ( + comment_create, comment_by_id, comment_set_spoiler, + comment_update, comment_delete, get_thread_participants +) from app.models.solution import get_solution_authors from app.models.crackme import ( - crackme_by_hexid, crackme_increment_comments, crackme_is_auto_validated + crackme_by_hexid, crackme_increment_comments, crackme_decrement_comments, + crackme_is_auto_validated ) from app.models.notification import notification_add from app.models.errors import ErrNoResult @@ -276,3 +280,118 @@ def mark_spoiler_by_token(comment_id, token): return render_template('comment/spoiler_marked.html', success=False, error="Comment not found.") + + +@comment_bp.route('/comment//edit', methods=['POST']) +@login_required +@limit("30 per hour", key_func=lambda: session.get('name')) +def edit_comment(comment_id): + """Edit a comment. + + Permissions: + - Comment author: can edit their own comment + """ + username = session.get('name') + + try: + comment = comment_by_id(comment_id) + except ErrNoResult: + flash('Comment not found.', FLASH_ERROR) + return redirect('/') + + crackme_hexid = comment.get('crackmehexid') + comment_author = comment.get('author') + + # Only comment author can edit + if comment_author != username: + flash('You can only edit your own comments.', FLASH_ERROR) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') + + # Validate required fields + is_valid, missing = validate_required(request.form, ['comment']) + if not is_valid: + flash(f'Field missing: {missing}', FLASH_ERROR) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') + + comment_text = bleach.clean(request.form.get('comment', '')) + if not comment_text.strip(): + flash('Comment cannot be empty.', FLASH_ERROR) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') + + old_content = comment.get('info', '') + + try: + comment_update(comment_id, comment_text) + except Exception as e: + print(f"Error updating comment: {e}") + flash('Failed to update comment. Please try again later.', FLASH_ERROR) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') + + # Handle any new @mentions added during editing + try: + old_mentions = parse_mentions(old_content) + new_mentions = parse_mentions(comment_text) - old_mentions + if new_mentions and crackme_hexid: + crackme = crackme_by_hexid(crackme_hexid) + crackme_author = crackme.get('author') + crackme_name = crackme['name'] + + thread_participants = get_thread_participants(crackme_hexid) + solution_authors = get_solution_authors(crackme_hexid) + valid_targets = thread_participants | solution_authors | {crackme_author} + + for mentioned_user in new_mentions: + if mentioned_user == username: + continue + if mentioned_user in valid_targets: + notification_add( + mentioned_user, + f"You were mentioned by {html_escape(username)} in a comment on '{html_escape(crackme_name)}'" + ) + except Exception as e: + print(f"Mention notification error on edit: {e}") + + flash('Comment updated!', FLASH_SUCCESS) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') + + +@comment_bp.route('/comment//delete', methods=['POST']) +@login_required +def delete_comment(comment_id): + """Delete a comment. + + Permissions: + - Comment author: can delete their own comment + """ + username = session.get('name') + + try: + comment = comment_by_id(comment_id) + except ErrNoResult: + flash('Comment not found.', FLASH_ERROR) + return redirect('/') + + crackme_hexid = comment.get('crackmehexid') + comment_author = comment.get('author') + + # Only comment author can delete + if comment_author != username: + flash('You can only delete your own comments.', FLASH_ERROR) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') + + try: + comment_delete(comment_id) + except Exception as e: + print(f"Error deleting comment: {e}") + flash('Failed to delete comment. Please try again later.', FLASH_ERROR) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') + + # Decrement comment count on crackme + if crackme_hexid: + try: + crackme_decrement_comments(crackme_hexid) + except Exception as e: + print(f"Failed to decrement comment count: {e}") + + flash('Comment deleted.', FLASH_SUCCESS) + return redirect(f'/crackme/{crackme_hexid}' if crackme_hexid else '/') diff --git a/app/models/comment.py b/app/models/comment.py index df8bf90..3234844 100644 --- a/app/models/comment.py +++ b/app/models/comment.py @@ -105,7 +105,10 @@ def comment_by_id(comment_id): collection = get_collection('comment') if isinstance(comment_id, str): - comment_id = ObjectId(comment_id) + try: + comment_id = ObjectId(comment_id) + except Exception: + raise ErrNoResult("Comment not found") comment = collection.find_one({'_id': comment_id}) if not comment: @@ -154,7 +157,10 @@ def comment_set_spoiler(comment_id, is_spoiler): collection = get_collection('comment') if isinstance(comment_id, str): - comment_id = ObjectId(comment_id) + try: + comment_id = ObjectId(comment_id) + except Exception: + raise ErrNoResult("Comment not found") result = collection.find_one_and_update( {'_id': comment_id}, @@ -166,3 +172,73 @@ def comment_set_spoiler(comment_id, is_spoiler): raise ErrNoResult("Comment not found") return result + + +def comment_update(comment_id, content): + """Update a comment's content. + + Args: + comment_id: The comment's ObjectId (string or ObjectId) + content: The new comment content + + Returns: + Updated comment document + + Raises: + ErrNoResult: If comment not found + """ + if not check_connection(): + raise ErrUnavailable("Database is unavailable") + + collection = get_collection('comment') + + if isinstance(comment_id, str): + try: + comment_id = ObjectId(comment_id) + except Exception: + raise ErrNoResult("Comment not found") + + result = collection.find_one_and_update( + {'_id': comment_id}, + {'$set': { + 'info': content, + 'edited': True, + 'updated_at': datetime.utcnow() + }}, + return_document=ReturnDocument.AFTER + ) + + if not result: + raise ErrNoResult("Comment not found") + + return result + + +def comment_delete(comment_id): + """Delete a comment by its ID. + + Args: + comment_id: The comment's ObjectId (string or ObjectId) + + Returns: + Deleted comment document + + Raises: + ErrNoResult: If comment not found + """ + if not check_connection(): + raise ErrUnavailable("Database is unavailable") + + collection = get_collection('comment') + + if isinstance(comment_id, str): + try: + comment_id = ObjectId(comment_id) + except Exception: + raise ErrNoResult("Comment not found") + + comment = collection.find_one_and_delete({'_id': comment_id}) + if not comment: + raise ErrNoResult("Comment not found") + + return comment diff --git a/templates/crackme/read.html b/templates/crackme/read.html index 0464f7b..010e0f4 100644 --- a/templates/crackme/read.html +++ b/templates/crackme/read.html @@ -43,6 +43,17 @@ } } + function openEditCommentModal(commentId) { + var raw = document.getElementById('comment-raw-' + commentId); + var textarea = document.getElementById('edit-comment-text'); + var form = document.getElementById('form-edit-comment'); + if (raw && textarea && form) { + textarea.value = raw.value; + form.action = '/comment/' + commentId + '/edit'; + openModal('modal-edit-comment'); + } + } + function closeModal() { document.querySelectorAll('.modal.active').forEach(function(m) { m.classList.remove('active'); @@ -320,8 +331,29 @@

{{ username }}'s {{ name }}

{% endif %} {% for comment in comments %}
- {% if usersess == username or (usersess == comment.author and not comment.spoiler) %}
{% endif %} - {{ comment.author }} on {{ comment.created_at|PRETTYTIME }}: +
+ {% if usersess == comment.author %} + +
+ + +
+ {% endif %} + {% if usersess == username or (usersess == comment.author and not comment.spoiler) %} +
+ + +
+ {% endif %} +
+ + {{ comment.author }} on {{ comment.created_at|PRETTYTIME }}{% if comment.edited or comment.updated_at %} (edited){% endif %}: {% if comment.spoiler %} [Click to reveal]{% else %}{{ comment.info|render_mentions }}{% endif %}
@@ -386,6 +418,29 @@

{{ username }}'s {{ name }}

{% endif %} + {% if not auto_validation %} + + {% endif %} + {% if not auto_validation %}