mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-08 04:51:42 +00:00
166 lines
7.1 KiB
Python
166 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Android Web Share Target hand-off.
|
|
|
|
The manifest points share_target at the watch list itself, so sharing a page from any Android
|
|
app arrives as GET /?pwa_preset_url=... and prefills the quick-add form's URL field. Everything
|
|
after that is the form that was already there - the processor radios pick the mode, the LLM
|
|
intent box is right underneath, and the watch is only created by the form's POST.
|
|
|
|
That "GET never creates anything" property is the reason this is safe to have on a plain
|
|
navigation that any app on the phone can trigger.
|
|
"""
|
|
|
|
from changedetectionio.blueprint.pwa.service import is_share, pwa_preset_url
|
|
|
|
|
|
def test_extracts_the_url_param():
|
|
assert pwa_preset_url({'pwa_preset_url': 'https://example.com/a.html'}) == 'https://example.com/a.html'
|
|
assert pwa_preset_url({'pwa_preset_url': ' https://example.com/a.html '}) == 'https://example.com/a.html'
|
|
|
|
|
|
def test_digs_the_url_out_of_shared_text():
|
|
"""Most Android apps put the link in the text body rather than the url field."""
|
|
assert pwa_preset_url({
|
|
'pwa_preset_text': 'Check this out https://example.com/deal?id=3 - cheap!'
|
|
}) == 'https://example.com/deal?id=3'
|
|
|
|
assert pwa_preset_url({
|
|
'pwa_preset_title': 'Look https://example.com/t'
|
|
}) == 'https://example.com/t'
|
|
|
|
# An explicit url param always wins over scraping prose
|
|
assert pwa_preset_url({
|
|
'pwa_preset_url': 'https://example.com/real',
|
|
'pwa_preset_text': 'https://example.com/decoy',
|
|
}) == 'https://example.com/real'
|
|
|
|
|
|
def test_schemeless_gets_a_scheme():
|
|
"""A share sheet hands over plenty of bare hostnames."""
|
|
assert pwa_preset_url({'pwa_preset_url': 'foobar.com/blah.html'}) == 'http://foobar.com/blah.html'
|
|
|
|
|
|
def test_trailing_sentence_punctuation_is_not_part_of_the_url():
|
|
"""The regex this replaced took the full stop with it, and the prefilled URL 404d -
|
|
on the one attempt most people will ever give this."""
|
|
assert pwa_preset_url({'pwa_preset_text': 'have a look at https://example.com/page.'}) \
|
|
== 'https://example.com/page'
|
|
assert pwa_preset_url({'pwa_preset_text': '(see https://example.com/x) ok'}) \
|
|
== 'https://example.com/x'
|
|
|
|
|
|
def test_brackets_belonging_to_the_url_are_kept():
|
|
assert pwa_preset_url({'pwa_preset_text': 'https://en.wikipedia.org/wiki/Foo_(bar) end'}) \
|
|
== 'https://en.wikipedia.org/wiki/Foo_(bar)'
|
|
|
|
|
|
def test_an_explicit_http_url_is_taken_verbatim():
|
|
"""Don't let the linkifier trim a trailing character an app deliberately sent."""
|
|
messy = 'https://www.amazon.de/dp/B08N5WRWNW?ref_=foo&th=1,'
|
|
assert pwa_preset_url({'pwa_preset_url': messy}) == messy
|
|
|
|
|
|
def test_non_web_links_are_not_offered():
|
|
"""Prefilling any of these is just noise in a box that only wants a web page."""
|
|
assert pwa_preset_url({'pwa_preset_text': 'mail me at contact@example.com'}) == ''
|
|
assert pwa_preset_url({'pwa_preset_text': 'ftp://files.example.com/x'}) == ''
|
|
assert pwa_preset_url({'pwa_preset_text': 'check //example.com/proto-relative'}) == ''
|
|
for hostile in ('javascript:alert(1)', 'data:text/html;base64,PHNjcmlwdD4=',
|
|
'file:///etc/passwd', 'JavaScript:alert(1)'):
|
|
assert pwa_preset_url({'pwa_preset_url': hostile}) == '', f"{hostile} was let through"
|
|
|
|
|
|
def test_a_plain_icon_launch_is_not_a_share():
|
|
"""start_url and share_target.action are the same URL, so the params are the only
|
|
signal - and a home screen launch must not produce share UI."""
|
|
assert is_share({}) is False
|
|
assert is_share({'tag': 'something', 'q': 'search'}) is False
|
|
assert is_share({'pwa_preset_text': 'shared a photo, no link'}) is True
|
|
|
|
|
|
def test_share_with_no_link_says_so(client, live_server):
|
|
res = client.get('/?pwa_preset_text=just+some+text+with+no+link', follow_redirects=True)
|
|
assert res.status_code == 200
|
|
assert b'Nothing shareable was found' in res.data
|
|
|
|
|
|
def test_plain_launch_is_quiet(client, live_server):
|
|
assert b'Nothing shareable was found' not in client.get('/', follow_redirects=True).data
|
|
|
|
|
|
def test_nothing_shared_is_not_an_error():
|
|
assert pwa_preset_url({}) == ''
|
|
assert pwa_preset_url({'pwa_preset_url': '', 'pwa_preset_text': 'no link in here'}) == ''
|
|
|
|
|
|
def test_share_prefills_the_quick_add_form(client, live_server):
|
|
res = client.get('/?pwa_preset_url=https://example.com/shared-page.html')
|
|
assert res.status_code == 200
|
|
assert b'value="https://example.com/shared-page.html"' in res.data, (
|
|
"shared URL did not reach the quick-add form's URL field"
|
|
)
|
|
|
|
|
|
def test_share_from_text_prefills_the_quick_add_form(client, live_server):
|
|
res = client.get('/?pwa_preset_text=have+a+look+https%3A%2F%2Fexample.com%2Ffrom-text+ok')
|
|
assert res.status_code == 200
|
|
assert b'value="https://example.com/from-text"' in res.data
|
|
|
|
|
|
def test_share_does_not_create_a_watch(client, live_server):
|
|
"""A GET that any installed app can trigger must not be able to add anything."""
|
|
before = client.get('/').data
|
|
|
|
client.get('/?pwa_preset_url=https://example.com/not-added.html')
|
|
|
|
after = client.get('/').data
|
|
assert b'https://example.com/not-added.html' not in after
|
|
assert before.count(b'watch-table') == after.count(b'watch-table')
|
|
|
|
|
|
def test_service_worker_is_served_from_the_root(client, live_server):
|
|
"""A worker can only control paths at or below its own URL. From /static/js/ its scope
|
|
would be /static/js/, no WebAPK would be built, and the share sheet entry never appears."""
|
|
res = client.get('/sw.js')
|
|
assert res.status_code == 200
|
|
assert res.headers['Content-Type'].startswith('text/javascript')
|
|
assert b"addEventListener('fetch'" in res.data, (
|
|
"a fetch handler is the part Chrome's install criteria have wanted"
|
|
)
|
|
assert 'no-cache' in res.headers['Cache-Control'], (
|
|
"a hard-cached worker script can't be updated"
|
|
)
|
|
|
|
|
|
def test_share_survives_the_login_redirect(app, client, live_server, datastore_path):
|
|
"""First run on a password-protected instance: install, share, get bounced to login.
|
|
|
|
unauthorized_handler used request.path, which drops the query string - so after logging
|
|
in the user landed on an empty watch list and the URL they just shared was gone. People
|
|
try this once.
|
|
"""
|
|
with app.test_client(use_cookies=True) as c:
|
|
res = c.post(
|
|
"/settings",
|
|
data={"application-password": "foobar",
|
|
"requests-time_between_check-minutes": 180,
|
|
'application-fetch_backend': "html_requests"},
|
|
follow_redirects=True
|
|
)
|
|
assert b"Password protection enabled." in res.data
|
|
|
|
try:
|
|
res = c.get('/?pwa_preset_url=https://example.com/shared.html')
|
|
assert res.status_code == 302
|
|
assert 'pwa_preset_url' in res.headers['Location'], (
|
|
f"shared URL dropped on the way to login: {res.headers['Location']}"
|
|
)
|
|
|
|
# ...and it's still there once the login page renders it back into its form
|
|
res = c.get(res.headers['Location'])
|
|
assert b'pwa_preset_url' in res.data
|
|
finally:
|
|
c.post("/settings", data={"application-removepassword_button": "Remove password"},
|
|
follow_redirects=True)
|