diff --git a/roles/modernpaste/files/paste.py b/roles/modernpaste/files/paste.py new file mode 100644 index 0000000000..3da551d8e6 --- /dev/null +++ b/roles/modernpaste/files/paste.py @@ -0,0 +1,249 @@ +import flask +from flask_login import current_user +from modern_paste import app + +import config +from uri.main import * +from uri.paste import * +from util.exception import * +from api.decorators import require_form_args +from api.decorators import require_login_api +from api.decorators import optional_login_api +import constants.api +import database.attachment +import database.paste +import database.user +import util.cryptography + +import datetime + +@app.route(PasteSubmitURI.path, methods=['POST']) +@require_form_args(['contents']) +@optional_login_api +def submit_paste(): + """ + Endpoint for submitting a new paste. + """ + if config.REQUIRE_LOGIN_TO_PASTE and not current_user.is_authenticated: + return ( + flask.jsonify(constants.api.UNAUTHENTICATED_PASTES_DISABLED_FAILURE), + constants.api.UNAUTHENTICATED_PASTES_DISABLED_FAILURE_CODE, + ) + + data = flask.request.get_json() + + if not config.ENABLE_PASTE_ATTACHMENTS and len(data.get('attachments', [])) > 0: + return ( + flask.jsonify(constants.api.PASTE_ATTACHMENTS_DISABLED_FAILURE), + constants.api.PASTE_ATTACHMENTS_DISABLED_FAILURE_CODE, + ) + + is_attachment_too_large = [ + # The data is encoded as a string: each character takes 1 B + # The base64-encoded string is at 4/3x larger in size than the raw file + len(attachment.get('data', '')) * 3 / 4.0 > config.MAX_ATTACHMENT_SIZE * 1000 * 1000 + for attachment in data.get('attachments', []) + ] + if any(is_attachment_too_large) and config.MAX_ATTACHMENT_SIZE > 0: + return ( + flask.jsonify(constants.api.PASTE_ATTACHMENT_TOO_LARGE_FAILURE), + constants.api.PASTE_ATTACHMENT_TOO_LARGE_FAILURE_CODE, + ) + + try: + new_paste = database.paste.create_new_paste( + contents=data.get('contents'), + user_id=current_user.user_id if current_user.is_authenticated else None, + #expiry_time=data.get('expiry_time'), + expiry_time=(datetime.datetime.now() + datetime.timedelta(weeks=1)).strftime('%s'), + title=data.get('title'), + language=data.get('language'), + password=data.get('password'), + # The paste is considered an API post if any of the following conditions are met: + # (1) The referrer is null. + # (2) The Home or PastePostInterface URIs are *not* contained within the referrer string (if a paste was + # posted via the web interface, this is where the user should be coming from, unless the client performed + # some black magic and spoofed the referrer string or something equally sketchy). + is_api_post=not flask.request.referrer or not any( + [uri in flask.request.referrer for uri in [HomeURI.full_uri(), PastePostInterfaceURI.full_uri()]] + ), + ) + new_attachments = [ + database.attachment.create_new_attachment( + paste_id=new_paste.paste_id, + file_name=attachment.get('name'), + file_size=attachment.get('size'), + mime_type=attachment.get('mime_type'), + file_data=attachment.get('data'), + ) + for attachment in data.get('attachments', []) + ] + resp_data = new_paste.as_dict().copy() + resp_data['attachments'] = [ + { + 'name': attachment.file_name, + 'size': attachment.file_size, + 'mime_type': attachment.mime_type, + } + for attachment in new_attachments + ] + return flask.jsonify(resp_data), constants.api.SUCCESS_CODE + except: + return flask.jsonify(constants.api.UNDEFINED_FAILURE), constants.api.UNDEFINED_FAILURE_CODE + + +@app.route(PasteDeactivateURI.path, methods=['POST']) +@require_form_args(['paste_id']) +@optional_login_api +def deactivate_paste(): + """ + Endpoint for deactivating an existing paste. + The user can deactivate a paste with this endpoint in two ways: + (1) Supply a deactivation token in the request, or + (2) Be currently logged in, and own the paste. + """ + data = flask.request.get_json() + try: + paste = database.paste.get_paste_by_id(util.cryptography.get_decid(data['paste_id']), active_only=True) + if (current_user.is_authenticated and current_user.user_id == paste.user_id) or data.get('deactivation_token') == paste.deactivation_token: + database.paste.deactivate_paste(paste.paste_id) + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_SUCCESS, + constants.api.MESSAGE: None, + 'paste_id': util.cryptography.get_id_repr(paste.paste_id), + }), constants.api.SUCCESS_CODE + fail_msg = 'User does not own requested paste' if current_user.is_authenticated else 'Deactivation token is invalid' + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_FAULURE, + constants.api.MESSAGE: fail_msg, + constants.api.FAILURE: 'auth_failure', + 'paste_id': util.cryptography.get_id_repr(paste.paste_id), + }), constants.api.AUTH_FAILURE_CODE + except (PasteDoesNotExistException, InvalidIDException): + return flask.jsonify(constants.api.NONEXISTENT_PASTE_FAILURE), constants.api.NONEXISTENT_PASTE_FAILURE_CODE + except: + return flask.jsonify(constants.api.UNDEFINED_FAILURE), constants.api.UNDEFINED_FAILURE_CODE + + +@app.route(PasteSetPasswordURI.path, methods=['POST']) +@require_form_args(['paste_id', 'password'], allow_blank_values=True) +@require_login_api +def set_paste_password(): + """ + Modify a paste's password, unset it, or set a new one. + """ + data = flask.request.get_json() + try: + paste = database.paste.get_paste_by_id(util.cryptography.get_decid(data['paste_id']), active_only=True) + if paste.user_id != current_user.user_id: + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_FAULURE, + constants.api.MESSAGE: 'User does not own the specified paste', + constants.api.FAILURE: 'auth_failure', + 'paste_id': util.cryptography.get_id_repr(paste.paste_id), + }), constants.api.AUTH_FAILURE_CODE + database.paste.set_paste_password(paste.paste_id, data['password']) + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_SUCCESS, + constants.api.MESSAGE: None, + 'paste_id': util.cryptography.get_id_repr(paste.paste_id), + }), constants.api.SUCCESS_CODE + except (PasteDoesNotExistException, InvalidIDException): + return flask.jsonify(constants.api.NONEXISTENT_PASTE_FAILURE), constants.api.NONEXISTENT_PASTE_FAILURE_CODE + except: + return flask.jsonify(constants.api.UNDEFINED_FAILURE), constants.api.UNDEFINED_FAILURE_CODE + + +@app.route(PasteDetailsURI.path, methods=['POST']) +@require_form_args(['paste_id']) +def paste_details(): + """ + Retrieve details for a particular paste ID. + """ + data = flask.request.get_json() + try: + paste = database.paste.get_paste_by_id(util.cryptography.get_decid(data['paste_id']), active_only=True) + attachments = database.attachment.get_attachments_for_paste(util.cryptography.get_decid(data['paste_id']), active_only=True) + paste_details_dict = paste.as_dict() + paste_details_dict['poster_username'] = 'Anonymous' + paste_details_dict['attachments'] = [ + attachment.as_dict() + for attachment in attachments + ] + if paste.user_id: + poster = database.user.get_user_by_id(paste.user_id) + paste_details_dict['poster_username'] = poster.username + if not paste.password_hash or (data.get('password') and paste.password_hash == util.cryptography.secure_hash(data.get('password'))): + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_SUCCESS, + constants.api.MESSAGE: None, + 'details': paste_details_dict, + }), constants.api.SUCCESS_CODE + else: + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_FAULURE, + constants.api.MESSAGE: 'Password-protected paste: either no password or wrong password supplied', + constants.api.FAILURE: 'password_mismatch_failure', + 'details': {}, + }), constants.api.AUTH_FAILURE_CODE + except (PasteDoesNotExistException, UserDoesNotExistException, InvalidIDException): + return flask.jsonify(constants.api.NONEXISTENT_PASTE_FAILURE), constants.api.NONEXISTENT_PASTE_FAILURE_CODE + except: + return flask.jsonify(constants.api.UNDEFINED_FAILURE), constants.api.UNDEFINED_FAILURE_CODE + + +@app.route(PastesForUserURI.path, methods=['POST']) +@require_login_api +def pastes_for_user(): + """ + Get all pastes for the currently logged in user. + """ + try: + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_SUCCESS, + constants.api.MESSAGE: None, + 'pastes': [ + paste.as_dict() + for paste in database.paste.get_all_pastes_for_user(current_user.user_id, active_only=True) + ], + }), constants.api.SUCCESS_CODE + except: + return flask.jsonify(constants.api.UNDEFINED_FAILURE), constants.api.UNDEFINED_FAILURE_CODE + + +@app.route(RecentPastesURI.path, methods=['POST']) +@require_form_args(['page_num', 'num_per_page']) +def recent_pastes(): + """ + Get details for the most recent pastes. + """ + try: + data = flask.request.get_json() + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_SUCCESS, + constants.api.MESSAGE: None, + 'pastes': [ + paste.as_dict() for paste in database.paste.get_recent_pastes(data['page_num'], data['num_per_page']) + ], + }), constants.api.SUCCESS_CODE + except: + return flask.jsonify(constants.api.UNDEFINED_FAILURE), constants.api.UNDEFINED_FAILURE_CODE + + +@app.route(TopPastesURI.path, methods=['POST']) +@require_form_args(['page_num', 'num_per_page']) +def top_pastes(): + """ + Get details for the top pastes. + """ + try: + data = flask.request.get_json() + return flask.jsonify({ + constants.api.RESULT: constants.api.RESULT_SUCCESS, + constants.api.MESSAGE: None, + 'pastes': [ + paste.as_dict() for paste in database.paste.get_top_pastes(data['page_num'], data['num_per_page']) + ], + }), constants.api.SUCCESS_CODE + except: + return flask.jsonify(constants.api.UNDEFINED_FAILURE), constants.api.UNDEFINED_FAILURE_CODE diff --git a/roles/modernpaste/files/post.html b/roles/modernpaste/files/post.html new file mode 100644 index 0000000000..21451dd1aa --- /dev/null +++ b/roles/modernpaste/files/post.html @@ -0,0 +1,121 @@ +{% extends 'base.html' %} + +{% block title %} + Modern Paste +{% endblock %} + +{% block head %} + {{ super() }} + + {{ import_css([ + 'lib/date-time-picker/jquery.datetimepicker.css', + 'lib/codemirror/lib/codemirror.css', + ])|safe }} + + {{ import_js([ + 'lib/date-time-picker/build/jquery.datetimepicker.full.min.js', + 'lib/codemirror/lib/codemirror.js', + 'paste/PostController.js', + ])|safe }} + + {% if config.BUILD_ENVIRONMENT == 'dev' %} + {% for language in config.LANGUAGES %} + {% if language != 'text' %} + {{ import_js(['lib/codemirror/mode/' ~ language ~ '/' ~ language ~ '.js'], defer=True)|safe }} + {% endif %} + {% endfor %} + {% else %} + {{ import_js(['paste/modes.js'], defer=True)|safe }} + {% endif %} +{% endblock %} + +{% block content %} + +
+

PASTE TITLE

+ +
+ + +
+

PASTE LANGUAGE

+ +
+ + +
+ + MORE OPTIONS + + + +

PASSWORD

+ +
+ + + {% if config.ENABLE_PASTE_ATTACHMENTS %} +
+

PASTE ATTACHMENTS

+
+ BROWSE FILES... + {% if config.MAX_ATTACHMENT_SIZE > 0 %} + Max size {{ config.MAX_ATTACHMENT_SIZE }} MB + {% endif %} +
+ + +
+ {% endif %} + + +
+

PASTE CONTENTS

+
+
+ + + + +
+
+

+
+ + +{% endblock %} diff --git a/roles/modernpaste/tasks/main.yml b/roles/modernpaste/tasks/main.yml index 063db63678..a9a235094d 100644 --- a/roles/modernpaste/tasks/main.yml +++ b/roles/modernpaste/tasks/main.yml @@ -25,6 +25,20 @@ - modernpaste notify: reload httpd +- name: Apply modernpaste hotfixes for forcing 1 week expiry (1) + copy: src=post.html dest=/usr/share/modern-paste/app/templates/paste/post.html owner=root group=root mode=644 + tags: + - hotfix + - modernpaste + notify: reload httpd + +- name: Apply modernpaste hotfixes for forcing 1 week expiry (2) + copy: src=paste.py dest=/usr/share/modern-paste/app/api/paste.py owner=root group=root mode=644 + tags: + - hotfix + - modernpaste + notify: reload httpd + - name: set sebooleans so paste can talk to the db seboolean: name=httpd_can_network_connect_db state=true persistent=true tags: