Compare commits
4 Commits
2df9ce4547
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b527bb200f | ||
|
|
13734e585b | ||
|
|
811e76797e | ||
|
|
0ae6521313 |
@@ -19,6 +19,11 @@ def create_app():
|
|||||||
response.headers['Cache-Control'] = f'public, max-age={max_age}'
|
response.headers['Cache-Control'] = f'public, max-age={max_age}'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def remove_content_disposition(response):
|
||||||
|
response.headers.pop('Content-Disposition', None)
|
||||||
|
return response
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
template_vars = {
|
template_vars = {
|
||||||
|
|||||||
71
app/api.py
71
app/api.py
@@ -14,6 +14,31 @@ ALLOWED_EXTENSIONS = {
|
|||||||
'svg', 'ico', 'woff', 'woff2', 'ttf', 'eot', 'json', 'map'
|
'svg', 'ico', 'woff', 'woff2', 'ttf', 'eot', 'json', 'map'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def sanitize_no_js(soup):
|
||||||
|
|
||||||
|
for script in soup.find_all('script'):
|
||||||
|
script.decompose()
|
||||||
|
|
||||||
|
dangerous_attrs = [
|
||||||
|
'onabort', 'onblur', 'onchange', 'onclick', 'ondblclick', 'onerror',
|
||||||
|
'onfocus', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onloadstart',
|
||||||
|
'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout',
|
||||||
|
'onmouseover', 'onmouseup', 'onmousewheel', 'onprogress', 'onreset',
|
||||||
|
'onresize', 'onscroll', 'onselect', 'onselectionchange', 'onstalled',
|
||||||
|
'onsubmit', 'onsuspend', 'ontimeupdate', 'onunload', 'onwaiting',
|
||||||
|
'onwheel'
|
||||||
|
]
|
||||||
|
|
||||||
|
for tag in soup.find_all(True):
|
||||||
|
cleaned_attrs = {}
|
||||||
|
for attr, value in tag.attrs.items():
|
||||||
|
attr_lower = attr.lower()
|
||||||
|
if not any(dangerous.startswith(attr_lower) for dangerous in dangerous_attrs):
|
||||||
|
cleaned_attrs[attr] = value
|
||||||
|
tag.attrs = cleaned_attrs
|
||||||
|
|
||||||
|
return soup
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
@@ -132,16 +157,44 @@ def resolve_css_imports(css_file_path, file_index):
|
|||||||
|
|
||||||
return parse_css_recursively(css_file_path)
|
return parse_css_recursively(css_file_path)
|
||||||
|
|
||||||
|
def sanitize_no_js(soup):
|
||||||
|
"""
|
||||||
|
Remove JS handlers
|
||||||
|
"""
|
||||||
|
for script in soup.find_all('script'):
|
||||||
|
script.decompose()
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
# for style in soup.find_all('style'):
|
||||||
|
# style.decompose()
|
||||||
|
|
||||||
|
dangerous_attrs = [
|
||||||
|
'onabort', 'onblur', 'onchange', 'onclick', 'ondblclick', 'onerror',
|
||||||
|
'onfocus', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onloadstart',
|
||||||
|
'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout',
|
||||||
|
'onmouseover', 'onmouseup', 'onmousewheel', 'onprogress', 'onreset',
|
||||||
|
'onresize', 'onscroll', 'onselect', 'onselectionchange', 'onstalled',
|
||||||
|
'onsubmit', 'onsuspend', 'ontimeupdate', 'onunload', 'onwaiting',
|
||||||
|
'onwheel'
|
||||||
|
]
|
||||||
|
|
||||||
|
for tag in soup.find_all(True):
|
||||||
|
cleaned_attrs = {}
|
||||||
|
for attr, value in tag.attrs.items():
|
||||||
|
attr_lower = attr.lower()
|
||||||
|
if not any(dangerous.startswith(attr_lower) for dangerous in dangerous_attrs):
|
||||||
|
cleaned_attrs[attr] = value
|
||||||
|
tag.attrs = cleaned_attrs
|
||||||
|
|
||||||
|
return soup
|
||||||
|
|
||||||
def make_aio_html(input_file, file_index):
|
def make_aio_html(input_file, file_index):
|
||||||
"""Generate basic AIO HTML with embedded resources."""
|
"""Generate basic AIO HTML with embedded resources (NO JS)."""
|
||||||
base_path = os.path.dirname(os.path.abspath(input_file))
|
base_path = os.path.dirname(os.path.abspath(input_file))
|
||||||
|
|
||||||
with open(input_file, 'r', encoding='utf-8', errors='ignore') as f:
|
with open(input_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
soup = BeautifulSoup(f.read(), 'html.parser')
|
soup = BeautifulSoup(f.read(), 'html.parser')
|
||||||
|
|
||||||
for script in soup.find_all('script', src=True):
|
|
||||||
script['src'] = embed_resource(base_path, script['src'], file_index)
|
|
||||||
|
|
||||||
for elem in soup.find_all(attrs={'src': True}):
|
for elem in soup.find_all(attrs={'src': True}):
|
||||||
elem['src'] = embed_resource(base_path, elem['src'], file_index)
|
elem['src'] = embed_resource(base_path, elem['src'], file_index)
|
||||||
|
|
||||||
@@ -149,10 +202,12 @@ def make_aio_html(input_file, file_index):
|
|||||||
if style.string:
|
if style.string:
|
||||||
style.string = embed_css_resources(base_path, style.string, file_index)
|
style.string = embed_css_resources(base_path, style.string, file_index)
|
||||||
|
|
||||||
|
soup = sanitize_no_js(soup)
|
||||||
|
|
||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
def make_aio_html_advanced(input_file, file_index):
|
def make_aio_html_advanced(input_file, file_index):
|
||||||
"""Advanced AIO: resolves @import CSS chains + embeds everything."""
|
"""Advanced AIO: resolves @import CSS chains + embeds (NO JS)."""
|
||||||
base_path = os.path.dirname(os.path.abspath(input_file))
|
base_path = os.path.dirname(os.path.abspath(input_file))
|
||||||
|
|
||||||
with open(input_file, 'r', encoding='utf-8', errors='ignore') as f:
|
with open(input_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
@@ -187,9 +242,6 @@ def make_aio_html_advanced(input_file, file_index):
|
|||||||
if href and not href.startswith(('http', 'data:')):
|
if href and not href.startswith(('http', 'data:')):
|
||||||
link.decompose()
|
link.decompose()
|
||||||
|
|
||||||
for script in soup.find_all('script', src=True):
|
|
||||||
script['src'] = embed_resource(base_path, script['src'], file_index)
|
|
||||||
|
|
||||||
for elem in soup.find_all(attrs={'src': True}):
|
for elem in soup.find_all(attrs={'src': True}):
|
||||||
elem['src'] = embed_resource(base_path, elem['src'], file_index)
|
elem['src'] = embed_resource(base_path, elem['src'], file_index)
|
||||||
|
|
||||||
@@ -197,8 +249,11 @@ def make_aio_html_advanced(input_file, file_index):
|
|||||||
if style.string:
|
if style.string:
|
||||||
style.string = embed_css_resources(base_path, style.string, file_index)
|
style.string = embed_css_resources(base_path, style.string, file_index)
|
||||||
|
|
||||||
|
soup = sanitize_no_js(soup)
|
||||||
|
|
||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route('/upload', methods=['POST'])
|
@api_bp.route('/upload', methods=['POST'])
|
||||||
def upload_files():
|
def upload_files():
|
||||||
return _upload_and_generate(make_aio_html)
|
return _upload_and_generate(make_aio_html)
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
exec gunicorn -w 4 -b 0.0.0.0:5000 'app:create_app()'
|
exec gunicorn -c gunicorn.conf.py --no-control-socket -w 4 -b 0.0.0.0:5000 'app:create_app()'
|
||||||
|
|||||||
10
gunicorn.conf.py
Normal file
10
gunicorn.conf.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from gunicorn.http import wsgi
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
def wrap_default_headers(func):
|
||||||
|
@wraps(func)
|
||||||
|
def default_headers(*args, **kwargs):
|
||||||
|
return [h for h in func(*args, **kwargs) if not h.startswith('Server:')]
|
||||||
|
return default_headers
|
||||||
|
|
||||||
|
wsgi.Response.default_headers = wrap_default_headers(wsgi.Response.default_headers)
|
||||||
@@ -5,3 +5,4 @@ python-dotenv==1.0.1
|
|||||||
werkzeug==3.0.4
|
werkzeug==3.0.4
|
||||||
gunicorn
|
gunicorn
|
||||||
cssutils
|
cssutils
|
||||||
|
flask-static-digest
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
.drop-zone {
|
.drop-zone {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
min-height: 280px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone:hover {
|
.drop-zone:hover {
|
||||||
|
|||||||
@@ -99,17 +99,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
<small><code>${escapeHtml(data.original ?? "")}</code> → <strong>${sizeKB}KB</strong></small>
|
<small><code>${escapeHtml(data.original ?? "")}</code> → <strong>${sizeKB}KB</strong></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group-vertical btn-group-sm">
|
<div class="btn-group-sm d-flex flex-column gap-2">
|
||||||
<button class="btn btn-success btn-copy-sm" type="button">
|
<button class="btn btn-success btn-copy-sm" type="button">
|
||||||
<i class="fa-solid fa-copy me-1"></i>Copy
|
<i class="fa-solid fa-copy me-1"></i>Copy
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a href="data:text/html;charset=utf-8,${encodeURIComponent(rawHtml)}"
|
||||||
href="data:text/html;charset=utf-8,${encodeURIComponent(rawHtml)}"
|
download="aio.html"
|
||||||
download="aio.html"
|
class="btn btn-outline-primary btn-sm">
|
||||||
class="btn btn-outline-primary btn-sm"
|
<i class="fa-solid fa-download me-1"></i>Save
|
||||||
>
|
|
||||||
<i class="fa-solid fa-download me-1"></i>Save
|
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,23 +46,23 @@
|
|||||||
<button class="btn btn-outline-primary btn-lg mode-btn active" data-mode="basic" type="button">
|
<button class="btn btn-outline-primary btn-lg mode-btn active" data-mode="basic" type="button">
|
||||||
<i class="fa-solid fa-bolt fa-2x"></i>
|
<i class="fa-solid fa-bolt fa-2x"></i>
|
||||||
<div class="fw-semibold">Basic</div>
|
<div class="fw-semibold">Basic</div>
|
||||||
<small class="text-muted">Fast</small>
|
<small class="text-muted">Fast - for small sites</small>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-outline-success btn-lg mode-btn" data-mode="advanced" type="button">
|
<button class="btn btn-outline-primary btn-lg mode-btn" data-mode="advanced" type="button">
|
||||||
<i class="fa-solid fa-gears fa-2x"></i>
|
<i class="fa-solid fa-gears fa-2x"></i>
|
||||||
<div class="fw-semibold">Advanced</div>
|
<div class="fw-semibold">Advanced</div>
|
||||||
<small class="text-muted">CSS chains</small>
|
<small class="text-muted">CSS chains - multiple css/js files</small>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Drop Zone -->
|
<!-- Drop Zone -->
|
||||||
<div id="drop-zone" class="drop-zone card border border-2 border-light rounded-4 p-5 mb-4 text-center">
|
<div id="drop-zone" class="drop-zone card border border-2 border-light rounded-4 p-4 mb-4 text-center">
|
||||||
<i class="fa-solid fa-cloud-arrow-up fa-4x text-muted mb-4"></i>
|
<i class="fa-solid fa-cloud-arrow-up fa-3x text-muted mb-4"></i>
|
||||||
<h3 class="h4 fw-bold mb-3">Drop files here</h3>
|
<h3 class="h4 fw-bold mb-3">Drop files here</h3>
|
||||||
<p class="text-muted mb-4">or click to browse</p>
|
<p class="text-muted mb-4">or click <i class="fa-solid fa-arrow-down"></i></p>
|
||||||
<input type="file" id="file-input" multiple class="d-none" accept="*" />
|
<input type="file" id="file-input" multiple class="d-none" accept="*" />
|
||||||
<label for="file-input" class="btn btn-primary btn-lg px-5">
|
<label for="file-input" class="btn btn-success btn-lg px-5">
|
||||||
<i class="fa-solid fa-folder-open me-2"></i>Choose Files
|
<i class="fa-solid fa-folder-open me-2"></i>Choose Files
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user