Skip to content

Commit d6eb98f

Browse files
authored
Better support for reverse-url construction containing whitespace (#1267)
1 parent f892936 commit d6eb98f

File tree

3 files changed

+140
-6
lines changed

3 files changed

+140
-6
lines changed

apprise/utils/parse.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -920,25 +920,31 @@ def parse_emails(*args, store_unparseable=True, **kwargs):
920920
return result
921921

922922

923-
def url_assembly(**kwargs):
923+
def url_assembly(encode=False, **kwargs):
924924
"""
925925
This function reverses the parse_url() function by taking in the provided
926926
result set and re-assembling a URL
927927
928928
"""
929+
def _no_encode(content, *args, **kwargs):
930+
# dummy function that does nothing to content
931+
return content
932+
933+
_quote = quote if encode else _no_encode
934+
929935
# Determine Authentication
930936
auth = ''
931937
if kwargs.get('user') is not None and \
932938
kwargs.get('password') is not None:
933939

934940
auth = '{user}:{password}@'.format(
935-
user=quote(kwargs.get('user'), safe=''),
936-
password=quote(kwargs.get('password'), safe=''),
941+
user=_quote(kwargs.get('user'), safe=''),
942+
password=_quote(kwargs.get('password'), safe=''),
937943
)
938944

939945
elif kwargs.get('user') is not None:
940946
auth = '{user}@'.format(
941-
user=quote(kwargs.get('user'), safe=''),
947+
user=_quote(kwargs.get('user'), safe=''),
942948
)
943949

944950
return '{schema}://{auth}{hostname}{port}{fullpath}{params}'.format(
@@ -948,7 +954,7 @@ def url_assembly(**kwargs):
948954
hostname='' if not kwargs.get('host') else kwargs.get('host', ''),
949955
port='' if not kwargs.get('port')
950956
else ':{}'.format(kwargs.get('port')),
951-
fullpath=quote(kwargs.get('fullpath', ''), safe='/'),
957+
fullpath=_quote(kwargs.get('fullpath', ''), safe='/'),
952958
params='' if not kwargs.get('qsd')
953959
else '?{}'.format(urlencode(kwargs.get('qsd'))),
954960
)

test/test_apprise_utils.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,27 @@ def test_parse_url_general():
773773
assert result['qsd']['+KeY'] == result['qsd+']['KeY']
774774
assert result['qsd']['-kEy'] == result['qsd-']['kEy']
775775

776+
# Testing Defect 1264 - whitespaces in url
777+
result = utils.parse.parse_url(
778+
'posts://example.com/my endpoint?-token=ab cdefg')
779+
780+
assert len(result['qsd-']) == 1
781+
assert len(result['qsd+']) == 0
782+
assert len(result['qsd']) == 1
783+
assert len(result['qsd:']) == 0
784+
785+
assert result['schema'] == 'posts'
786+
assert result['host'] == 'example.com'
787+
assert result['port'] is None
788+
assert result['user'] is None
789+
assert result['password'] is None
790+
assert result['fullpath'] == '/my%20endpoint'
791+
assert result['path'] == '/'
792+
assert result['query'] == "my%20endpoint"
793+
assert result['url'] == 'posts://example.com/my%20endpoint'
794+
assert '-token' in result['qsd']
795+
assert result['qsd-']['token'] == 'ab cdefg'
796+
776797

777798
def test_parse_url_simple():
778799
"utils: parse_url() testing """
@@ -1181,6 +1202,38 @@ def test_url_assembly():
11811202
assert utils.parse.url_assembly(
11821203
**utils.parse.parse_url(url, verify_host=False)) == url
11831204

1205+
# When spaces and special characters are introduced, the URL
1206+
# is hard to mimic what was entered. Instead it is normalized
1207+
url = 'schema://hostname:10/a space/file.php?' \
1208+
'arg=a+space&arg2=a%20space&arg3=a space'
1209+
assert utils.parse.url_assembly(
1210+
**utils.parse.parse_url(url, verify_host=False)) == \
1211+
'schema://hostname:10/a%20space/file.php?' \
1212+
'arg=a%2Bspace&arg2=a+space&arg3=a+space'
1213+
1214+
# encode=True should only be used if you're passing in un-assembled
1215+
# content... hence the following is likely not what is expected:
1216+
assert utils.parse.url_assembly(
1217+
**utils.parse.parse_url(url, verify_host=False), encode=True) == \
1218+
'schema://hostname:10/a%2520space/file.php?' \
1219+
'arg=a%2Bspace&arg2=a+space&arg3=a+space'
1220+
1221+
# But the following utilizes the encode=True and produces the
1222+
# desired effects:
1223+
content = {
1224+
'host': 'hostname',
1225+
# Note that fullpath requires escaping in this case
1226+
'fullpath': '/a space/file.php',
1227+
'path': '/a space/',
1228+
'query': 'file.php',
1229+
'schema': 'schema',
1230+
# our query arguments also require escaping as well
1231+
'qsd': {'arg': 'a+space', 'arg2': 'a space', 'arg3': 'a space'},
1232+
}
1233+
assert utils.parse.url_assembly(**content, encode=True) == \
1234+
'schema://hostname/a%20space/file.php?' \
1235+
'arg=a%2Bspace&arg2=a+space&arg3=a+space'
1236+
11841237

11851238
def test_parse_bool():
11861239
"utils: parse_bool() testing """

test/test_decorator_notify.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,81 @@ def my_inline_notify_wrapper(
431431
N_MGR.remove('utiltest')
432432

433433

434+
def test_notify_decorator_urls_with_space():
435+
"""decorators: URLs containing spaces
436+
"""
437+
# This is in relation to https://github.com/caronc/apprise/issues/1264
438+
439+
# Verify our schema we're about to declare doesn't already exist
440+
# in our schema map:
441+
assert 'post' not in N_MGR
442+
443+
verify_obj = []
444+
445+
@notify(on="posts")
446+
def apprise_custom_api_call_wrapper(
447+
body, title, notify_type, attach, meta, *args, **kwargs):
448+
449+
# Track what is added
450+
verify_obj.append({
451+
'body': body,
452+
'title': title,
453+
'notify_type': notify_type,
454+
'attach': attach,
455+
'meta': meta,
456+
'args': args,
457+
'kwargs': kwargs,
458+
})
459+
460+
assert 'posts' in N_MGR
461+
462+
# Create ourselves an apprise object
463+
aobj = Apprise()
464+
465+
# Add our configuration
466+
aobj.add("posts://example.com/my endpoint?-token=ab cdefg")
467+
468+
# We loaded 1 item
469+
assert len(aobj) == 1
470+
471+
# Nothing stored yet in our object
472+
assert len(verify_obj) == 0
473+
474+
# Send utf-8 characters
475+
assert aobj.notify("ツ".encode('utf-8'), title="My Title") is True
476+
477+
# Service notified
478+
assert len(verify_obj) == 1
479+
480+
# Extract our object
481+
obj = verify_obj.pop()
482+
483+
assert obj.get('body') == 'ツ'
484+
assert obj.get('title') == 'My Title'
485+
assert obj.get('notify_type') == 'info'
486+
assert obj.get('attach') is None
487+
assert isinstance(obj.get('args'), tuple)
488+
assert len(obj.get('args')) == 0
489+
assert obj.get('kwargs') == {'body_format': None}
490+
meta = obj.get('meta')
491+
assert isinstance(meta, dict)
492+
493+
assert meta.get('schema') == 'posts'
494+
assert meta.get('url') == \
495+
'posts://example.com/my%20endpoint?-token=ab+cdefg'
496+
assert meta.get('qsd') == {'-token': 'ab cdefg'}
497+
assert meta.get('host') == 'example.com'
498+
assert meta.get('fullpath') == '/my%20endpoint'
499+
assert meta.get('path') == '/'
500+
assert meta.get('query') == 'my%20endpoint'
501+
assert isinstance(meta.get('tag'), set)
502+
assert len(meta.get('tag')) == 0
503+
assert isinstance(meta.get('asset'), AppriseAsset)
504+
505+
# Tidy
506+
N_MGR.remove('posts')
507+
508+
434509
def test_notify_multi_instance_decoration(tmpdir):
435510
"""decorators: Test multi-instance @notify
436511
"""
@@ -481,7 +556,7 @@ def my_inline_notify_wrapper(
481556
# The number of configuration files that exist
482557
assert len(ac) == 1
483558

484-
# no notifications are loaded
559+
# 2 notification endpoints are loaded
485560
assert len(ac.servers()) == 2
486561

487562
# Nothing stored yet in our object

0 commit comments

Comments
 (0)