Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,6 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install Selenium
run: |
curl -LsSfO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y
if: matrix.os == 'ubuntu-latest'
- uses: astral-sh/setup-uv@v7
- run: uv run pytest -m selenium
- uses: codecov/codecov-action@v5
Expand Down
76 changes: 75 additions & 1 deletion s3file/forms.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import base64
import html
import logging
import pathlib
import uuid
from html.parser import HTMLParser

from django.conf import settings
from django.templatetags.static import static
from django.utils.functional import cached_property
from django.utils.html import format_html, html_safe
from django.utils.safestring import mark_safe
from storages.utils import safe_join

from s3file.middleware import S3FileMiddleware
Expand All @@ -15,6 +18,71 @@
logger = logging.getLogger("s3file")


class InputToS3FileRewriter(HTMLParser):
"""HTML parser that rewrites <input type="file"> to <s3-file> custom elements."""

def __init__(self):
super().__init__()
self.output = []

def handle_starttag(self, tag, attrs):
if tag == "input" and dict(attrs).get("type") == "file":
self.output.append("<s3-file")
for name, value in attrs:
if name != "type":
self.output.append(
f' {name}="{html.escape(value, quote=True)}"'
if value
else f" {name}"
)
self.output.append(">")
else:
self.output.append(self.get_starttag_text())

def handle_endtag(self, tag):
self.output.append(f"</{tag}>")

def handle_data(self, data):
self.output.append(data)

def handle_startendtag(self, tag, attrs):
if tag == "input" and dict(attrs).get("type") == "file":
self.output.append("<s3-file")
for name, value in attrs:
if name != "type":
self.output.append(
f' {name}="{html.escape(value, quote=True)}"'
if value
else f" {name}"
)
self.output.append(">")
else:
self.output.append(self.get_starttag_text())

def handle_comment(self, data):
# Preserve HTML comments in the output
self.output.append(f"<!--{data}-->")

def handle_decl(self, decl):
# Preserve declarations such as <!DOCTYPE ...> in the output
self.output.append(f"<!{decl}>")

def handle_pi(self, data):
# Preserve processing instructions such as <?xml ...?> in the output
self.output.append(f"<?{data}>")

def handle_entityref(self, name):
# Preserve HTML entities like &amp;, &lt;, &gt;
self.output.append(f"&{name};")

def handle_charref(self, name):
# Preserve character references like &#39;, &#x27;
self.output.append(f"&#{name};")

def get_html(self):
return "".join(self.output)


@html_safe
class Asset:
"""A generic asset that can be included in a template."""
Expand Down Expand Up @@ -75,7 +143,6 @@ def client(self):

def build_attrs(self, *args, **kwargs):
attrs = super().build_attrs(*args, **kwargs)
attrs["is"] = "s3-file"

accept = attrs.get("accept")
response = self.client.generate_presigned_post(
Expand All @@ -97,6 +164,13 @@ def build_attrs(self, *args, **kwargs):

return defaults

def render(self, name, value, attrs=None, renderer=None):
"""Render the widget as a custom element for Safari compatibility."""
html_output = str(super().render(name, value, attrs=attrs, renderer=renderer))
parser = InputToS3FileRewriter()
parser.feed(html_output)
return mark_safe(parser.get_html()) # noqa: S308

def get_conditions(self, accept):
conditions = [
{"bucket": self.bucket_name},
Expand Down
Loading
Loading