Merged in feature/HTTPASTE-16/controller/ui (pull request #45)

Feature/HTTPASTE-16/controller/ui
This commit is contained in:
Tiara Rodney 2022-04-16 04:31:48 +00:00
commit c645478b98
24 changed files with 698 additions and 9 deletions

View file

@ -40,4 +40,5 @@ where = src
[options.package_data]
* =
*.json
*.sql
*.sql
*.html

View file

@ -15,4 +15,4 @@ def get(**kwargs):
paste_lifetime=model.paste.default_lifetime,
paste_max_lifetime=str(round(model.paste.default_max_lifetime / 60)),
paste_default_encoding=model.paste.default_encoding
), 200
), 302, {'Location': '/ui'}

View file

@ -0,0 +1,13 @@
from httpaste.helper.template import views
from httpaste import __doc__ as man_page
def search(**kwargs):
template = views.get_template("viewport/ui/search.html")
variables = {
'paste_index_url': '/ui/paste',
'man_page': man_page
}
return template.render(**variables), 200

View file

@ -0,0 +1,90 @@
from io import BytesIO
from base64 import b64encode
from connexion import request
from httpaste.helper.template import views
from httpaste.helper.url import url_query_string, url_append_query_param
from httpaste.controller.paste import post as post_raw
from httpaste.controller.paste import get as get_raw
def search(**kwargs):
template = views.get_template("viewport/ui/paste/search.html")
variables = {
'create_public_paste_url': '/ui/paste/public',
'create_private_paste_url': '/ui/paste/private',
'user': kwargs.get('user'),
'delete_session_url': '/ui/user/session/delete'
}
return template.render(**variables), 200
def post(**kwargs):
#rewriting strict form to mixed (as expected by cascaded controller)
data = kwargs['body'].pop('data')
kwargs = {**kwargs, **kwargs['body']}
kwargs.pop('body')
kwargs['body'] = {'data': data}
#prepare octet stream data for cascaded controller
if kwargs.get('data').filename:
bfr = BytesIO()
kwargs.get('data').save(bfr)
bfr.seek(0)
kwargs['body']['data'] = b64encode(bfr.read()).decode('utf-8')
kwargs['encoding'] = 'base64'
output, status_code = post_raw(**kwargs)
#TODO: lifetime=-1 no preview handler
url = output.strip('\n')
if kwargs.get('lifetime') and int(kwargs['lifetime']) < 0:
url = url_append_query_param(url, 'preview', 'False')
return output, 302, {'Location': url}
def get(**kwargs):
template = views.get_template("viewport/ui/paste/get.html")
base_path = f'paste/public/{kwargs["id"]}'
raw_paste_url = f'{request.host_url}{base_path}'
if kwargs.get('user'):
raw_paste_url = f'{request.host_url}{base_path}'
paste_url = raw_paste_url
paste_url_query = {}
for field in ['format', 'mime', 'syntax']:
if kwargs.get(field):
paste_url_query[field] = kwargs[field]
if paste_url_query:
paste_url = '?'.join((paste_url, url_query_string(paste_url_query)))
preview_url = f'/ui/{base_path}'
if kwargs.get('preview'):
paste_url_query['preview'] = kwargs['preview']
preview_url = '?'.join((preview_url, url_query_string(paste_url_query)))
variables = {
'raw_paste_url': raw_paste_url,
'paste_url': paste_url,
'preview_url': preview_url,
'query': {
'format': kwargs.get('format', ''),
'syntax': kwargs.get('syntax', ''),
'mime': kwargs.get('mime', ''),
'preview': kwargs.get('preview', True)
}
}
return template.render(**variables)

View file

@ -0,0 +1,24 @@
from httpaste.helper.template import views
from httpaste.controller.ui.paste import post as post_proxy
from httpaste.controller.ui.paste import get as get_proxy
def search(**kwargs):
template = views.get_template("viewport/ui/paste/private/search.html")
variables = {
'paste_form_url': '/ui/paste/private',
'user': kwargs.get('user')
}
return template.render(**variables), 200
def post(**kwargs):
return post_proxy(**kwargs)
def get(**kwargs):
return get_proxy(**kwargs)

View file

@ -0,0 +1,23 @@
from httpaste.helper.template import views
from httpaste.controller.ui.paste import post as post_proxy
from httpaste.controller.ui.paste import get as get_proxy
def search(**kwargs):
template = views.get_template("viewport/ui/paste/public/search.html")
variables = {
'paste_form_url': '/ui/paste/public'
}
return template.render(**variables), 200
def post(**kwargs):
return post_proxy(**kwargs)
def get(**kwargs):
return get_proxy(**kwargs)

View file

@ -0,0 +1,19 @@
from httpaste.helper.template import views
from httpaste.controller.user.session import delete as raw_delete
from connexion import request
def search(**kwargs):
template = views.get_template("viewport/ui/user/session/search.html")
print(request.path)
variables = {'session_delete_url': request.path + '/delete'}
return template.render(**variables), 200
def delete(**kwargs):
return raw_delete(**kwargs)

View file

@ -0,0 +1,6 @@
from httpaste.helper.template import views
from httpaste.controller.ui.user.session import delete as proxy_delete
def search(**kwargs):
return proxy_delete(**kwargs)

View file

@ -2,7 +2,7 @@
"""
from flask import current_app
from httpaste.helper.http import ForbiddenError
from httpaste.helper.http import UnauthorizedError
from httpaste.model.user import authenticate, AuthenticationError
from httpaste.backend import load_backend
@ -22,4 +22,11 @@ def post(*args, **kwargs):
return authenticate(user_id, password, backend.user, context)
except AuthenticationError as e:
raise ForbiddenError('You shall not pass!') from e
raise UnauthorizedError('You shall not pass!') from e
def delete(**kwargs):
"""
"""
raise UnauthorizedError('Authentication Rejection requested by client')

View file

@ -21,7 +21,7 @@ class UnauthorizedError(RuntimeError):
return {
"detail": str(error),
"status": 401,
"title": "Unauthorized s",
"title": "Unauthorized",
}, 401

View file

@ -0,0 +1,6 @@
from jinja2 import Environment, PackageLoader, select_autoescape
views = Environment(
loader=PackageLoader("httpaste", "views"),
autoescape=select_autoescape()
)

View file

@ -0,0 +1,20 @@
from urllib.parse import urlparse, parse_qs
def url_query_string(fields:dict):
return '&'.join([f'{k}={v}' for k,v in fields.items()])
def url_append_query_param(url:str, name: str, value:str):
urlcomps = urlparse(url)
q = parse_qs(urlcomps.query)
q[name] = value
qs = url_query_string(q)
return urlcomps._replace(query=qs).geturl()

View file

@ -18,7 +18,7 @@
"get": {
"description": "get description",
"responses": {
"200": {
"303": {
"description": "",
"content": {
"text/plain": {
@ -185,6 +185,258 @@
}
}
}
},
"/ui": {
"get": {
"description": "create a new public paste",
"security": [
{}
],
"responses": {
"200": {
"description": "paste location",
"content": {
"text/html": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ui/paste": {
"get": {
"description": "create a new public paste",
"security": [
{}
],
"responses": {
"200": {
"description": "paste location",
"content": {
"text/html": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ui/paste/public": {
"get": {
"description": "create a new public paste UI-driven",
"security": [
{}
],
"responses": {
"200": {
"description": "paste location",
"content": {
"text/html": {
"schema": {
"type": "string"
}
}
}
}
}
},
"post": {
"description": "create a new public paste",
"requestBody": {
"$ref": "#/components/requestBodies/pastePost"
},
"security": [
{}
],
"parameters": [
{
"$ref": "#/components/parameters/lifetime"
}
],
"responses": {
"200": {
"description": "paste location",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PasteURL"
}
}
}
}
}
}
},
"/ui/paste/private": {
"get": {
"description": "create a new public paste UI-driven",
"security": [
{
"basicAuth": []
}
],
"responses": {
"200": {
"description": "paste location",
"content": {
"text/html": {
"schema": {
"type": "string"
}
}
}
}
}
},
"post": {
"description": "create a new public paste",
"requestBody": {
"$ref": "#/components/requestBodies/pastePost"
},
"security": [
{
"basicAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/lifetime"
},
{
"$ref": "#/components/parameters/encoding"
}
],
"responses": {
"200": {
"description": "paste location",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PasteURL"
}
}
}
}
}
}
},
"/ui/paste/public/{id}": {
"get": {
"description": "get a public paste",
"security": [
{}
],
"parameters": [
{
"$ref": "#/components/parameters/id"
},
{
"$ref": "#/components/parameters/syntax"
},
{
"$ref": "#/components/parameters/format"
},
{
"$ref": "#/components/parameters/linenos"
},
{
"$ref": "#/components/parameters/mime"
},
{
"$ref": "#/components/parameters/ui_preview"
}
],
"responses": {
"200": {
"description": "paste data. content type may vary.",
"content": {
"text/html": {
"schema": {
"$ref": "#/components/schemas/PasteData"
}
}
}
}
}
}
},
"/ui/paste/private/{id}": {
"get": {
"description": "get a public paste",
"security": [
{
"basicAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/id"
},
{
"$ref": "#/components/parameters/syntax"
},
{
"$ref": "#/components/parameters/format"
},
{
"$ref": "#/components/parameters/linenos"
},
{
"$ref": "#/components/parameters/mime"
}
],
"responses": {
"200": {
"description": "paste data. content type may vary.",
"content": {
"text/html": {
"schema": {
"$ref": "#/components/schemas/PasteData"
}
}
}
}
}
}
},
"/ui/user/session": {
"get": {
"description": "get a public paste",
"security": [
{
"basicAuth": []
}
],
"responses": {
"200": {
"description": "paste data. content type may vary.",
"content": {
"text/html": {}
}
}
}
}
},
"/ui/user/session/delete": {
"get": {
"description": "get a public paste",
"security": [
{}
],
"responses": {
"200": {
"description": "paste data. content type may vary.",
"content": {
"text/html": {}
}
}
}
}
}
},
"components": {
@ -215,9 +467,9 @@
"type": "string",
"format": "binary"
},
"rsa_public_key": {
"description": "RSA public key",
"type": "string"
"fileName": {
"type": "string",
"format": "binary"
}
},
"required": [
@ -294,6 +546,15 @@
"schema": {
"type": "string"
}
},
"ui_preview": {
"description": "enable preview in UI",
"name": "preview",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
}
}
},
"securitySchemes": {

View file

@ -0,0 +1,26 @@
<form class="color-theme font-theme" action="{{preview_url}}" method="get" enctype="multipart/form-data">
<details>
<summary>
<label for="syntax">Syntax</label>
<input type="text" name="syntax" value="{{ query['syntax'] }}"/>
</summary>
<a href="https://pygments.org/docs/lexers/" target="_blank">Pygments lexer short name</a> <i>(e.g. 'terraform', 'python')</i>
</details>
<details>
<summary>
<label for="format">Format</label>
<input type="text" name="format" value="{{ query['format'] }}"/>
</summary>
<a href="https://pygments.org/docs/formatters/" target="_blank">Pygments formatter short name</a> <i>(e.g. 'html', 'terminal256')</i>
</details>
<details>
<summary>
<label for="mime">Content-Type (MIME)</label>
<input type="text" name="mime" value="{{ query['mime'] }}"/>
</summary>
Content-Type Header the server should return
</details>
<input type="text" name="preview" value="{{ query['preview'] }}" class="hidden"/>
<input type="submit" value="☝ Refresh"/>
</form>

View file

@ -0,0 +1,21 @@
<form class="color-theme font-theme" action="{{paste_form_url}}" method="post" enctype="multipart/form-data" target="_top">
<details>
<summary>
<label for="data">Data</label>
<div><textarea name="data" cols="60" rows="10" style="width:100%"></textarea><br/>
<input type="file" id="myfile" name="data"/>
</div>
</summary>
Either supply a past text, or upload a file.
</details>
<details>
<summary>
<label for="lifetime">Lifetime</label>
<input type="number" id="lname" name="lifetime" value="5">
</summary>
Set a pastes lifetime to make it expire after a specified amount of time.<br/>
The lifetime must be provided in minutes and cannot be less than 1<br/>(, unless lesser than 0).<br/>
A lifetime of 0 will evaluate to a lifetime 1.
</details>
<input type="submit" value="☝ Paste"/>
</form>

View file

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style>
html, body {
margin: 0;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: left;
}
.hidden {
display: none;
}
.color-theme details {
background-color: #EEE;
}
.font-theme details {
font-style: italic;
}
.color-theme details > summary {
background-color: white;
}
.font-theme details > summary {
font-style: normal;
}
details {
margin-bottom: 1em;
}
details > summary {
list-style-type: '?';
}
</style>
</head>
<body>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style>
html, body {
margin: 0;
height: 100%;
width: 100%;
display: flex;
}
main {
width: 100%;
display: flex;
flex-direction: column;
}
.iframe {
width: 100%;
flex-grow: 1;
border: none;
}
</style>
</head>
<body>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View file

@ -0,0 +1,27 @@
{% extends 'frame/base.html' %}
{% block content %}
<a href="/">Return</a>
<h1>Paste Conditioner</h1>
{% if query['preview'] %}
Preview
<iframe src="{{paste_url}}" title="as" style="resize:both; width: 100%"></iframe>
<hr/>
{% else %}
<p><b>Preview is disabled.</b>
<br/>
This probably happened because the paste is set to expire after read.
<br/>
<i>You can still proceed to condition the paste URL.</i>
</p>
{% endif %}
{% include 'container/get_paste_form.html' %}
<div style="justify-content: center">
<hr/>
<h3>Paste URLs</h3>
Formatted: <a href="{{paste_url}}" target="_top">{{paste_url}}</a>
<br/>
Raw: <a href="{{raw_paste_url}}" target="_top">{{raw_paste_url}}</a>
</div>
{% endblock %}

View file

@ -0,0 +1,5 @@
{% extends 'frame/base.html' %}
{% block content %}
{% include 'container/post_paste_form.html' %}
{% endblock %}

View file

@ -0,0 +1,5 @@
{% extends 'frame/base.html' %}
{% block content %}
{% include 'container/post_paste_form.html' %}
{% endblock %}

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style>
html, body {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<p>
<a href="{{create_private_paste_url}}">Create a Private Paste</a>
</p>
<p>
<a href="{{create_public_paste_url}}">Create a Public Paste</a>
</p>
<p>
<a href="{{ delete_session_url }}" target="_blank" style="text-align: center">
Flush Local HTTP Authentication Cache
</a>
</p>
</body>
</html>

View file

@ -0,0 +1,11 @@
{% extends 'frame/decorated.html' %}
{% block content %}
<header style="display: flex; justify-content: center">
<details>
<summary style="text-align: center"><a href="/">httpaste - versatile HTTP pastebin (User Interface)</a></summary>
<textarea readonly="true" style="width: 100%">{{ man_page }}</textarea>
</details>
</header>
<iframe src="{{ paste_index_url }}" title="" class="iframe"></iframe>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends 'frame/base.html' %}
{% block content %}
<a href="{{ session_delete_url }}">Clear Local HTTP Authentication Cache</a>
{% endblock %}