Skip to content
Open
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: 5 additions & 0 deletions common/djangoapps/static_replace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import re
from urllib.parse import unquote

from django.conf import settings
from django.contrib.staticfiles import finders
Expand Down Expand Up @@ -216,6 +217,10 @@ def replace_static_url(original, prefix, quote, rest):
from common.djangoapps.static_replace.models import AssetBaseUrlConfig, AssetExcludedExtensionsConfig
base_url = AssetBaseUrlConfig.get_base_url()
excluded_exts = AssetExcludedExtensionsConfig.get_excluded_extensions()
# TinyMCE percent-encodes unicode characters (e.g. é -> %C3%A9) in asset
# filenames. Decode before building the asset key so get_canonicalized_asset_path
# doesn't double-encode the '%' into '%25'.
rest = unquote(rest)
url = StaticContent.get_canonicalized_asset_path(course_id, rest, base_url, excluded_exts)

if AssetLocator.CANONICAL_NAMESPACE in url:
Expand Down
35 changes: 35 additions & 0 deletions common/djangoapps/static_replace/test/test_static_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,41 @@ def test_mongo_filestore(mock_get_excluded_extensions, mock_get_base_url, mock_m
mock_static_content.get_canonicalized_asset_path.assert_called_once_with(COURSE_KEY, 'file.png', '', ['foobar'])


@patch('common.djangoapps.static_replace.StaticContent', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
@patch('common.djangoapps.static_replace.models.AssetBaseUrlConfig.get_base_url')
@patch('common.djangoapps.static_replace.models.AssetExcludedExtensionsConfig.get_excluded_extensions')
def test_replace_static_url_with_percent_encoded_unicode(
mock_get_excluded_extensions, mock_get_base_url, mock_modulestore, mock_static_content
):
"""
Test that percent-encoded unicode characters in static URLs are not double-encoded.

TinyMCE encodes é as %C3%A9; replace_static_urls should decode it before building the
asset key so get_canonicalized_asset_path does not re-encode the '%' into '%25'.
"""
mock_modulestore.return_value = Mock(MongoModuleStore)
# Echo the decoded rest back so the output reflects what was passed to the asset key builder.
mock_static_content.get_canonicalized_asset_path.side_effect = \
lambda course_id, rest, base_url, excluded_exts: f"c4x://mock/{rest}"
mock_get_base_url.return_value = ''
mock_get_excluded_extensions.return_value = ['foobar']

# Input: TinyMCE-encoded é (%C3%A9)
html = '<img src="/static/Se_prot%C3%A9ger.png" />'
result = replace_static_urls(html, DATA_DIRECTORY, course_id=COURSE_KEY)

# Must NOT contain double-encoded %25C3%25A9
assert '%25C3%25A9' not in result
# Must contain the decoded é (single-encoded %C3%A9 or the raw é)
assert '%C3%A9' in result or 'é' in result

# The decoded filename must be passed to get_canonicalized_asset_path
mock_static_content.get_canonicalized_asset_path.assert_called_once_with(
COURSE_KEY, 'Se_protéger.png', '', ['foobar']
)


@patch('common.djangoapps.static_replace.settings', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
@patch('common.djangoapps.static_replace.staticfiles_storage', autospec=True)
Expand Down
Loading