Skip to content

Commit df4a32c

Browse files
committed
Merge branch 'hotfix/url-quoting'
[SVCS-682] Closes: #330
2 parents 0e33dcb + 9547ce4 commit df4a32c

File tree

9 files changed

+117
-33
lines changed

9 files changed

+117
-33
lines changed

mfr/extensions/audio/render.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from mako.lookup import TemplateLookup
44

55
from mfr.core import extension
6+
from mfr.extensions.utils import escape_url_for_template
67

78

89
class AudioRenderer(extension.BaseRenderer):
@@ -13,7 +14,8 @@ class AudioRenderer(extension.BaseRenderer):
1314
]).get_template('viewer.mako')
1415

1516
def render(self):
16-
return self.TEMPLATE.render(base=self.assets_url, url=self.url)
17+
safe_url = escape_url_for_template(self.url)
18+
return self.TEMPLATE.render(base=self.assets_url, url=safe_url)
1719

1820
@property
1921
def file_required(self):

mfr/extensions/image/render.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from mfr.core import extension
77
from mfr.extensions.image import settings
8-
from mfr.extensions.utils import munge_url_for_localdev
8+
from mfr.extensions.utils import munge_url_for_localdev, escape_url_for_template
99

1010

1111
class ImageRenderer(extension.BaseRenderer):
@@ -19,7 +19,8 @@ def render(self):
1919
self.metrics.add('needs_export', False)
2020
if self.metadata.ext in settings.EXPORT_EXCLUSIONS:
2121
download_url = munge_url_for_localdev(self.url)
22-
return self.TEMPLATE.render(base=self.assets_url, url=download_url.geturl())
22+
safe_url = escape_url_for_template(download_url.geturl())
23+
return self.TEMPLATE.render(base=self.assets_url, url=safe_url)
2324

2425
exported_url = furl.furl(self.export_url)
2526
if settings.EXPORT_MAXIMUM_SIZE and settings.EXPORT_TYPE:
@@ -28,10 +29,12 @@ def render(self):
2829
exported_url.args['format'] = settings.EXPORT_TYPE
2930
else:
3031
download_url = munge_url_for_localdev(self.url)
31-
return self.TEMPLATE.render(base=self.assets_url, url=download_url.geturl())
32+
safe_url = escape_url_for_template(download_url.geturl())
33+
return self.TEMPLATE.render(base=self.assets_url, url=safe_url)
3234

3335
self.metrics.add('needs_export', True)
34-
return self.TEMPLATE.render(base=self.assets_url, url=exported_url.url)
36+
safe_url = escape_url_for_template(exported_url.url)
37+
return self.TEMPLATE.render(base=self.assets_url, url=safe_url)
3538

3639
@property
3740
def file_required(self):

mfr/extensions/jsc3d/render.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from mfr.core import extension
99
from mfr.extensions.jsc3d import settings
10-
from mfr.extensions.utils import munge_url_for_localdev
10+
from mfr.extensions.utils import munge_url_for_localdev, escape_url_for_template
1111

1212

1313
class JSC3DRenderer(extension.BaseRenderer):
@@ -21,18 +21,20 @@ def render(self):
2121
self.metrics.add('needs_export', False)
2222
if self.metadata.ext in settings.EXPORT_EXCLUSIONS:
2323
download_url = munge_url_for_localdev(self.metadata.download_url)
24+
safe_url = escape_url_for_template(download_url.geturl())
2425
return self.TEMPLATE.render(
2526
base=self.assets_url,
26-
url=download_url.geturl(),
27+
url=safe_url,
2728
ext=self.metadata.ext.lower(),
2829
)
2930

3031
exported_url = furl.furl(self.export_url)
3132
exported_url.args['format'] = settings.EXPORT_TYPE
3233
self.metrics.add('needs_export', True)
34+
safe_url = escape_url_for_template(exported_url.url)
3335
return self.TEMPLATE.render(
3436
base=self.assets_url,
35-
url=exported_url.url,
37+
url=safe_url,
3638
ext=settings.EXPORT_TYPE,
3739
)
3840

mfr/extensions/pdb/render.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from mfr.core import extension
88
from mfr.extensions.pdb import settings
9-
from mfr.extensions.utils import munge_url_for_localdev
9+
from mfr.extensions.utils import munge_url_for_localdev, escape_url_for_template
1010

1111

1212
class PdbRenderer(extension.BaseRenderer):
@@ -18,9 +18,10 @@ class PdbRenderer(extension.BaseRenderer):
1818

1919
def render(self):
2020
download_url = munge_url_for_localdev(self.metadata.download_url)
21+
safe_url = escape_url_for_template(download_url.geturl())
2122
return self.TEMPLATE.render(
2223
base=self.assets_url,
23-
url=download_url.geturl(),
24+
url=safe_url,
2425
options=json.dumps(settings.OPTIONS),
2526
)
2627

mfr/extensions/pdf/render.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from mfr.core import extension
88
from mfr.extensions.pdf import settings
9-
from mfr.extensions.utils import munge_url_for_localdev
9+
from mfr.extensions.utils import munge_url_for_localdev, escape_url_for_template
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -19,11 +19,16 @@ class PdfRenderer(extension.BaseRenderer):
1919
]).get_template('viewer.mako')
2020

2121
def render(self):
22+
2223
download_url = munge_url_for_localdev(self.metadata.download_url)
23-
logger.debug('extension::{} supported-list::{}'.format(self.metadata.ext, settings.EXPORT_SUPPORTED))
24+
logger.debug('extension::{} supported-list::{}'.format(self.metadata.ext,
25+
settings.EXPORT_SUPPORTED))
2426
if self.metadata.ext not in settings.EXPORT_SUPPORTED:
2527
logger.debug('Extension not found in supported list!')
26-
return self.TEMPLATE.render(base=self.assets_url, url=download_url.geturl())
28+
return self.TEMPLATE.render(
29+
base=self.assets_url,
30+
url=escape_url_for_template(download_url.geturl())
31+
)
2732

2833
logger.debug('Extension found in supported list!')
2934
exported_url = furl.furl(self.export_url)
@@ -35,9 +40,16 @@ def render(self):
3540
exported_url.args['format'] = settings.EXPORT_TYPE
3641

3742
self.metrics.add('needs_export', True)
38-
return self.TEMPLATE.render(base=self.assets_url, url=exported_url.url)
39-
40-
return self.TEMPLATE.render(base=self.assets_url, url=download_url.geturl())
43+
return self.TEMPLATE.render(
44+
base=self.assets_url,
45+
url=escape_url_for_template(exported_url.url)
46+
)
47+
48+
# TODO: is this dead code? ``settings.EXPORT_TYPE`` is never None or empty
49+
return self.TEMPLATE.render(
50+
base=self.assets_url,
51+
url=escape_url_for_template(download_url.geturl())
52+
)
4153

4254
@property
4355
def file_required(self):

mfr/extensions/svg/render.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from mako.lookup import TemplateLookup
55

66
from mfr.core import extension
7+
from mfr.extensions.utils import escape_url_for_template
78

89

910
class SvgRenderer(extension.BaseRenderer):
@@ -14,7 +15,8 @@ class SvgRenderer(extension.BaseRenderer):
1415
]).get_template('viewer.mako')
1516

1617
def render(self):
17-
return self.TEMPLATE.render(base=self.assets_url, url=self.url)
18+
safe_url = escape_url_for_template(self.url)
19+
return self.TEMPLATE.render(base=self.assets_url, url=safe_url)
1820

1921
@property
2022
def file_required(self):

mfr/extensions/utils.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
1+
import logging
2+
from typing import Tuple
13
from urllib.parse import parse_qs, urlencode, urlparse
24

35
from mfr.extensions import settings
46

7+
logger = logging.getLogger(__name__)
58

6-
def munge_url_for_localdev(url):
7-
"""
8-
If MFR is being run in a local development environment (i.e. LOCAL_DEVELOPMENT is True), we
9+
10+
def munge_url_for_localdev(url: str) -> Tuple:
11+
"""If MFR is being run in a local development environment (i.e. LOCAL_DEVELOPMENT is True), we
912
need to replace the internal host (the one the backend services communicate on, default:
1013
192.168.168.167) with the external host (the one the user provides, default: "localhost")
1114
e.g. http://192.168.168.167:7777/foo/bar => http://localhost:7777/foo/bar
1215
"""
16+
1317
url_obj = urlparse(url)
14-
if (settings.LOCAL_DEVELOPMENT and url_obj.hostname == settings.DOCKER_LOCAL_HOST):
15-
query_dict = parse_qs(url_obj.query, keep_blank_values=True)
18+
if settings.LOCAL_DEVELOPMENT and url_obj.hostname == settings.DOCKER_LOCAL_HOST:
19+
query_dict = parse_qs(url_obj.query, keep_blank_values=True)
1620

17-
# the 'mode' param will break image downloads from the osf
18-
query_dict.pop('mode', None)
21+
# the 'mode' param will break image downloads from the osf
22+
query_dict.pop('mode', None)
1923

20-
url_obj = url_obj._replace(
21-
query=urlencode(query_dict, doseq=True),
22-
netloc='{}:{}'.format(settings.LOCAL_HOST, url_obj.port)
23-
)
24+
url_obj = url_obj._replace(
25+
query=urlencode(query_dict, doseq=True),
26+
netloc='{}:{}'.format(settings.LOCAL_HOST, url_obj.port)
27+
)
2428

2529
return url_obj
30+
31+
32+
def escape_url_for_template(url: str, logs: bool=True) -> str:
33+
"""Escape (URL Encode) single and double quote(s) for the given URL.
34+
35+
Download and export URLs may end up not properly encoded right before they are about to be sent
36+
to the mako template due to issues including (but not limited to) (1) ``furl`` dropping encoding
37+
for single quote (2) URL (provided by users or constructed by scripts) not having the correct
38+
encoding. This helper method must be called for each render request that sends URL to the mako
39+
template.
40+
41+
:param str url: the URL to be sent to the mako template
42+
:param bool logs: whether to enable warnings, default is `True` and is set to `False` for tests
43+
:return: the properly encoded URL
44+
"""
45+
46+
safe_url = url.replace('"', '%22').replace("'", '%27')
47+
if url != safe_url and logs:
48+
logger.warning('Unsafe URL containing unescaped single (double) quote(s) has been replaced')
49+
return safe_url

mfr/extensions/video/render.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from mako.lookup import TemplateLookup
44

55
from mfr.core import extension
6-
from mfr.extensions.utils import munge_url_for_localdev
6+
from mfr.extensions.utils import munge_url_for_localdev, escape_url_for_template
77

88

99
class VideoRenderer(extension.BaseRenderer):
@@ -15,7 +15,8 @@ class VideoRenderer(extension.BaseRenderer):
1515

1616
def render(self):
1717
download_url = munge_url_for_localdev(self.metadata.download_url)
18-
return self.TEMPLATE.render(url=download_url.geturl())
18+
safe_url = escape_url_for_template(download_url.geturl())
19+
return self.TEMPLATE.render(url=safe_url)
1920

2021
@property
2122
def file_required(self):

tests/extensions/pdf/test_renderer.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
import furl
22
import pytest
33

4-
from mfr.extensions.pdf import (settings,
5-
PdfRenderer)
64
from mfr.core.provider import ProviderMetadata
5+
from mfr.extensions.pdf import settings, PdfRenderer
6+
from mfr.extensions.utils import escape_url_for_template
77

88

99
@pytest.fixture
1010
def metadata():
1111
return ProviderMetadata('test', '.pdf', 'text/plain', '1234',
1212
'http://wb.osf.io/file/test.pdf?token=1234')
1313

14+
1415
@pytest.fixture
1516
def tif_metadata():
1617
return ProviderMetadata('test', '.tif', 'text/plain', '1234',
1718
'http://wb.osf.io/file/test.tif?token=1234')
1819

20+
1921
@pytest.fixture
2022
def file_path():
2123
return '/tmp/test.pdf'
2224

25+
2326
@pytest.fixture
2427
def tif_file_path():
2528
return '/tmp/test.tif'
@@ -42,13 +45,14 @@ def assets_url():
4245

4346
@pytest.fixture
4447
def export_url():
45-
return 'http://mfr.osf.io/export?url=' + url()
48+
return 'http://mfr.osf.io/export?url=http://osf.io/file/test.pdf'
4649

4750

4851
@pytest.fixture
4952
def renderer(metadata, file_path, url, assets_url, export_url):
5053
return PdfRenderer(metadata, file_path, url, assets_url, export_url)
5154

55+
5256
@pytest.fixture
5357
def tif_renderer(tif_metadata, tif_file_path, tif_url, assets_url, export_url):
5458
return PdfRenderer(tif_metadata, tif_file_path, tif_url, assets_url, export_url)
@@ -62,6 +66,23 @@ def test_render_pdf(self, renderer, metadata, assets_url):
6266
assert '<div id="viewer" class="pdfViewer"></div>' in body
6367
assert 'DEFAULT_URL = \'{}\''.format(metadata.download_url) in body
6468

69+
def test_render_pdf_with_single_quote_in_name(self, assets_url):
70+
71+
download_url = 'http://wb.osf.io/file/te\'st.pdf?token=1234'
72+
safe_download_url = 'http://wb.osf.io/file/te%27st.pdf?token=1234'
73+
74+
metadata = ProviderMetadata('te\'st', '.pdf', 'text/plain', '1234', download_url)
75+
renderer = PdfRenderer(metadata, '/tmp/te\'st.pdf', 'http://osf.io/file/te\'st.pdf',
76+
assets_url,
77+
'http://mfr.osf.io/export?url=http://osf.io/file/te\'st.pdf')
78+
79+
body = renderer.render()
80+
81+
assert '<base href="{}/{}/web/" target="_blank">'.format(assets_url, 'pdf') in body
82+
assert '<div id="viewer" class="pdfViewer"></div>' in body
83+
assert 'DEFAULT_URL = \'{}\''.format(download_url) not in body
84+
assert 'DEFAULT_URL = \'{}\''.format(safe_download_url) in body
85+
6586
def test_render_tif(self, tif_renderer, assets_url):
6687
exported_url = furl.furl(tif_renderer.export_url)
6788
exported_url.args['format'] = '{}.{}'.format(settings.EXPORT_MAXIMUM_SIZE,
@@ -71,3 +92,19 @@ def test_render_tif(self, tif_renderer, assets_url):
7192
assert '<base href="{}/{}/web/" target="_blank">'.format(assets_url, 'pdf') in body
7293
assert '<div id="viewer" class="pdfViewer"></div>' in body
7394
assert 'DEFAULT_URL = \'{}\''.format(exported_url.url) in body
95+
96+
def test_render_docx(self, assets_url):
97+
98+
export_url = 'http://mfr.osf.io/export?url=http://osf.io/file/te\'st.docx&format=pdf'
99+
safe_url = 'http://mfr.osf.io/export?url=http://osf.io/file/te%27st.docx&format=pdf'
100+
101+
metadata = ProviderMetadata('te\'st', '.docx', 'text/plain', '1234', export_url)
102+
renderer = PdfRenderer(metadata, '/tmp/te\'st.docx', export_url, assets_url,
103+
'http://mfr.osf.io/export?url=http://osf.io/file/te\'st.docx')
104+
105+
body = renderer.render()
106+
107+
assert '<base href="{}/{}/web/" target="_blank">'.format(assets_url, 'pdf') in body
108+
assert '<div id="viewer" class="pdfViewer"></div>' in body
109+
assert 'DEFAULT_URL = \'{}\''.format(export_url) not in body
110+
assert 'DEFAULT_URL = \'{}\''.format(safe_url) in body

0 commit comments

Comments
 (0)