Repository: saleor/saleor

Issue:
Bug: Double-encoded file urls
### What are you trying to achieve?

File attribute values are double-encoded when space is in the file name. The returned file URL is not working.

### Steps to reproduce the problem

1. Create file attribute
2. Create product type and assigned newly created file attribute to product attributes
3. Create a product with a newly assigned product type
4. Add a file with space in the name as the file attribute value
5. Try to open the saved file

### What did you expect to happen?

The valid URL should be returned.

### Logs

_No response_

### Environment

Saleor version: 3.5+



Discussion:

Resolving patch:
diff --git a/saleor/graphql/core/mutations.py b/saleor/graphql/core/mutations.py
--- a/saleor/graphql/core/mutations.py
+++ b/saleor/graphql/core/mutations.py
@@ -759,6 +759,11 @@ def perform_mutation(cls, _root, info, **data):
 
         # add unique text fragment to the file name to prevent file overriding
         file_name, format = os.path.splitext(file_data._name)
+
+        # replace spaced with an underscore to prevent replacing the spaces with encoded
+        # values by storage
+        file_name = file_name.replace(" ", "_")
+
         hash = secrets.token_hex(nbytes=4)
         new_name = f"file_upload/{file_name}_{hash}{format}"
 
diff --git a/saleor/graphql/core/types/common.py b/saleor/graphql/core/types/common.py
--- a/saleor/graphql/core/types/common.py
+++ b/saleor/graphql/core/types/common.py
@@ -1,4 +1,4 @@
-from urllib.parse import urlparse
+from urllib.parse import unquote, urlparse
 
 import graphene
 from django.core.files.storage import default_storage
@@ -448,7 +448,8 @@ def resolve_url(root, info):
         # check if URL is absolute:
         if urlparse(root.url).netloc:
             return root.url
-        return build_absolute_uri(default_storage.url(root.url))
+        # unquote used for preventing double URL encoding
+        return build_absolute_uri(default_storage.url(unquote(root.url)))
 
 
 class PriceInput(graphene.InputObjectType):


Test patch:
diff --git a/saleor/graphql/core/tests/test_file_upload.py b/saleor/graphql/core/tests/test_file_upload.py
--- a/saleor/graphql/core/tests/test_file_upload.py
+++ b/saleor/graphql/core/tests/test_file_upload.py
@@ -153,3 +153,56 @@ def test_file_upload_file_with_the_same_name_already_exists(
     assert file_url != f"http://{domain}/media/{image_file._name}"
     assert file_url != f"http://{domain}/media/{path}"
     assert default_storage.exists(file_url.replace(f"http://{domain}/media/", ""))
+
+
+def test_file_upload_file_name_with_space(staff_api_client, media_root):
+    # given
+    image_file, image_name = create_image("file name with spaces")
+    variables = {"image": image_name}
+    body = get_multipart_request_body(
+        FILE_UPLOAD_MUTATION, variables, image_file, image_name
+    )
+
+    # when
+    response = staff_api_client.post_multipart(body)
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["fileUpload"]
+    errors = data["errors"]
+
+    assert not errors
+    assert data["uploadedFile"]["contentType"] == "image/png"
+    file_name, format = os.path.splitext(image_file._name)
+    file_name = file_name.replace(" ", "_")
+    returned_url = data["uploadedFile"]["url"]
+    file_path = urlparse(returned_url).path
+    assert file_path.startswith(f"/media/file_upload/{file_name}")
+    assert file_path.endswith(format)
+    assert default_storage.exists(file_path.lstrip("/media"))
+
+
+def test_file_upload_file_name_with_encoded_value(staff_api_client, media_root):
+    # given
+    image_file, image_name = create_image("file%20name")
+    variables = {"image": image_name}
+    body = get_multipart_request_body(
+        FILE_UPLOAD_MUTATION, variables, image_file, image_name
+    )
+
+    # when
+    response = staff_api_client.post_multipart(body)
+
+    # then
+    content = get_graphql_content(response)
+    data = content["data"]["fileUpload"]
+    errors = data["errors"]
+
+    assert not errors
+    assert data["uploadedFile"]["contentType"] == "image/png"
+    file_name, format = os.path.splitext(image_file._name)
+    returned_url = data["uploadedFile"]["url"]
+    file_path = urlparse(returned_url).path
+    assert file_path.startswith(f"/media/file_upload/{file_name}")
+    assert file_path.endswith(format)
+    assert default_storage.exists(file_path.lstrip("/media"))