2015-09-24 17:56:33 +02:00
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
import io
|
|
|
|
import textwrap
|
|
|
|
import re
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
from qutebrowser.misc import mhtml
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def patch_uuid(monkeypatch):
|
|
|
|
monkeypatch.setattr("uuid.uuid4", lambda: "UUID")
|
|
|
|
|
|
|
|
|
|
|
|
class Checker:
|
|
|
|
|
|
|
|
"""A helper to check mhtml output.
|
|
|
|
|
|
|
|
Attrs:
|
|
|
|
fp: A BytesIO object for passing to MHTMLWriter.write_to.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.fp = io.BytesIO()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return self.fp.getvalue()
|
|
|
|
|
|
|
|
def expect(self, expected):
|
|
|
|
actual = self.value.decode("ascii")
|
|
|
|
# Make sure there are no stray \r or \n
|
|
|
|
assert re.search(r"\r[^\n]", actual) is None
|
|
|
|
assert re.search(r"[^\r]\n", actual) is None
|
|
|
|
actual = actual.replace("\r\n", "\n")
|
|
|
|
expected = textwrap.dedent(expected).lstrip("\n")
|
|
|
|
assert expected == actual
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def checker():
|
|
|
|
return Checker()
|
|
|
|
|
|
|
|
|
|
|
|
def test_quoted_printable_umlauts(checker):
|
|
|
|
content = "Die süße Hündin läuft in die Höhle des Bären"
|
|
|
|
content = content.encode("iso-8859-1")
|
|
|
|
writer = mhtml.MHTMLWriter(root_content=content,
|
|
|
|
content_location="localhost",
|
|
|
|
content_type="text/plain")
|
|
|
|
writer.write_to(checker.fp)
|
|
|
|
checker.expect("""
|
2015-09-24 20:43:30 +02:00
|
|
|
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
|
|
|
MIME-Version: 1.0
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: localhost
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
Die=20s=FC=DFe=20H=FCndin=20l=E4uft=20in=20die=20H=F6hle=20des=20B=E4ren
|
|
|
|
-----=_qute-UUID--
|
|
|
|
""")
|
2015-09-24 17:56:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("header, value", [
|
|
|
|
("content_location", "http://brötli.com"),
|
|
|
|
("content_type", "text/pläin"),
|
|
|
|
])
|
|
|
|
def test_refuses_non_ascii_header_value(checker, header, value):
|
|
|
|
defaults = {
|
|
|
|
"root_content": b"",
|
|
|
|
"content_location": "http://example.com",
|
|
|
|
"content_type": "text/plain",
|
|
|
|
}
|
|
|
|
defaults[header] = value
|
|
|
|
writer = mhtml.MHTMLWriter(**defaults)
|
2015-09-24 20:43:30 +02:00
|
|
|
with pytest.raises(UnicodeEncodeError) as excinfo:
|
2015-09-24 17:56:33 +02:00
|
|
|
writer.write_to(checker.fp)
|
2015-09-24 20:43:30 +02:00
|
|
|
assert "'ascii' codec can't encode" in str(excinfo.value)
|
2015-09-24 17:56:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_file_encoded_as_base64(checker):
|
|
|
|
content = b"Image file attached"
|
|
|
|
writer = mhtml.MHTMLWriter(root_content=content, content_type="text/plain",
|
|
|
|
content_location="http://example.com")
|
|
|
|
writer.add_file(location="http://a.example.com/image.png",
|
|
|
|
content="\U0001F601 image data".encode("utf-8"),
|
|
|
|
content_type="image/png",
|
|
|
|
transfer_encoding=mhtml.E_BASE64)
|
|
|
|
writer.write_to(checker.fp)
|
|
|
|
checker.expect("""
|
2015-09-24 20:43:30 +02:00
|
|
|
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
|
|
|
MIME-Version: 1.0
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://example.com
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
Image=20file=20attached
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://a.example.com/image.png
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: image/png
|
|
|
|
Content-Transfer-Encoding: base64
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
8J+YgSBpbWFnZSBkYXRh
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
-----=_qute-UUID--
|
|
|
|
""")
|
2015-09-24 17:56:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("transfer_encoding", [mhtml.E_BASE64, mhtml.E_QUOPRI],
|
|
|
|
ids=["base64", "quoted-printable"])
|
|
|
|
def test_payload_lines_wrap(checker, transfer_encoding):
|
|
|
|
payload = b"1234567890" * 10
|
|
|
|
writer = mhtml.MHTMLWriter(root_content=b"", content_type="text/plain",
|
|
|
|
content_location="http://example.com")
|
|
|
|
writer.add_file(location="http://example.com/payload", content=payload,
|
|
|
|
content_type="text/plain",
|
|
|
|
transfer_encoding=transfer_encoding)
|
|
|
|
writer.write_to(checker.fp)
|
|
|
|
for line in checker.value.split(b"\r\n"):
|
|
|
|
assert len(line) < 77
|
|
|
|
|
|
|
|
|
|
|
|
def test_files_appear_sorted(checker):
|
|
|
|
writer = mhtml.MHTMLWriter(root_content=b"root file",
|
|
|
|
content_type="text/plain",
|
|
|
|
content_location="http://www.example.com/")
|
|
|
|
for subdomain in "ahgbizt":
|
|
|
|
writer.add_file(location="http://{}.example.com/".format(subdomain),
|
|
|
|
content="file {}".format(subdomain).encode("utf-8"),
|
|
|
|
content_type="text/plain",
|
|
|
|
transfer_encoding=mhtml.E_QUOPRI)
|
|
|
|
writer.write_to(checker.fp)
|
|
|
|
checker.expect("""
|
2015-09-24 20:43:30 +02:00
|
|
|
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
|
|
|
MIME-Version: 1.0
|
|
|
|
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://www.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
root=20file
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://a.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
file=20a
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://b.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
file=20b
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://g.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
file=20g
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://h.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
file=20h
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://i.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
file=20i
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://t.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
file=20t
|
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://z.example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
|
|
|
|
|
|
|
file=20z
|
|
|
|
-----=_qute-UUID--
|
|
|
|
""")
|
2015-09-24 17:56:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_empty_content_type(checker):
|
|
|
|
writer = mhtml.MHTMLWriter(root_content=b"",
|
|
|
|
content_location="http://example.com/",
|
|
|
|
content_type="text/plain")
|
|
|
|
writer.add_file("http://example.com/file", b"file content")
|
|
|
|
writer.write_to(checker.fp)
|
|
|
|
checker.expect("""
|
2015-09-24 20:43:30 +02:00
|
|
|
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
|
|
|
MIME-Version: 1.0
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
2015-09-24 17:56:33 +02:00
|
|
|
|
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
-----=_qute-UUID
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Location: http://example.com/file
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
file=20content
|
|
|
|
-----=_qute-UUID--
|
|
|
|
""")
|
2015-09-24 17:56:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_removing_file_from_mhtml(checker):
|
|
|
|
writer = mhtml.MHTMLWriter(root_content=b"root",
|
|
|
|
content_location="http://example.com/",
|
|
|
|
content_type="text/plain")
|
|
|
|
writer.add_file("http://evil.com/", b"file content")
|
|
|
|
writer.remove_file("http://evil.com/")
|
|
|
|
writer.write_to(checker.fp)
|
|
|
|
checker.expect("""
|
2015-09-24 20:43:30 +02:00
|
|
|
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
|
|
|
MIME-Version: 1.0
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
-----=_qute-UUID
|
|
|
|
Content-Location: http://example.com/
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: text/plain
|
|
|
|
Content-Transfer-Encoding: quoted-printable
|
2015-09-24 17:56:33 +02:00
|
|
|
|
2015-09-24 20:43:30 +02:00
|
|
|
root
|
|
|
|
-----=_qute-UUID--
|
|
|
|
""")
|
2015-09-24 17:56:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("style, expected_urls", [
|
|
|
|
("@import 'default.css'", ["default.css"]),
|
|
|
|
('@import "default.css"', ["default.css"]),
|
|
|
|
("@import \t 'tabbed.css'", ["tabbed.css"]),
|
|
|
|
("@import url('default.css')", ["default.css"]),
|
|
|
|
("""body {
|
|
|
|
background: url("/bg-img.png")
|
|
|
|
}""", ["/bg-img.png"]),
|
|
|
|
("background: url(folder/file.png)", ["folder/file.png"]),
|
|
|
|
("content: url()", []),
|
|
|
|
])
|
|
|
|
def test_css_url_scanner(style, expected_urls):
|
|
|
|
expected_urls.sort()
|
|
|
|
urls = mhtml._get_css_imports(style)
|
|
|
|
urls.sort()
|
|
|
|
assert urls == expected_urls
|
2015-09-24 20:43:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_noclose_bytesio_fake_close():
|
|
|
|
fp = mhtml._NoCloseBytesIO()
|
|
|
|
fp.write(b'Value')
|
|
|
|
fp.close()
|
|
|
|
assert fp.getvalue() == b'Value'
|
|
|
|
fp.write(b'Eulav')
|
|
|
|
assert fp.getvalue() == b'ValueEulav'
|
|
|
|
|
|
|
|
|
|
|
|
def test_noclose_bytesio_actual_close():
|
|
|
|
fp = mhtml._NoCloseBytesIO()
|
|
|
|
fp.write(b'Value')
|
|
|
|
fp.actual_close()
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
|
|
fp.getvalue()
|
|
|
|
assert 'I/O operation on closed file.' == str(excinfo.value)
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
|
|
fp.write(b'Closed')
|
|
|
|
assert 'I/O operation on closed file.' == str(excinfo.value)
|