Compare commits

...

4 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
b527bb200f remove js handlers 2026-02-26 15:59:44 +01:00
Mateusz Gruszczyński
13734e585b small 2026-02-26 15:15:46 +01:00
Mateusz Gruszczyński
811e76797e use advenced as default 2026-02-26 14:46:36 +01:00
Mateusz Gruszczyński
0ae6521313 use advenced as default 2026-02-26 14:45:45 +01:00
8 changed files with 97 additions and 27 deletions

View File

@@ -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 = {

View File

@@ -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)

View File

@@ -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
View 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)

View File

@@ -5,3 +5,4 @@ python-dotenv==1.0.1
werkzeug==3.0.4 werkzeug==3.0.4
gunicorn gunicorn
cssutils cssutils
flask-static-digest

View File

@@ -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 {

View File

@@ -99,20 +99,19 @@ 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>
<div class="row g-3"> <div class="row g-3">
<div class="col-12 col-xl-6"> <div class="col-12 col-xl-6">

View File

@@ -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>