We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Bug: Lack of validation in cleaning manifest data
### What are you trying to achieve?

The `KeyError` is raised when `manifest_data` doesn't have `tokenTargetUrl`.

### Steps to reproduce the problem

1. Run `AppFetchManifest` with URL that contains JSON data without `tokenTargetUrl`.
2. You will get the `KeyError`.

### What did you expect to happen?

The `ValidationError` should be raised.

### Logs

https://sentry.io/organizations/saleor/issues/3749157627/?project=6417854

### Environment

Saleor version: 3.9 (to check if it also affect other versions)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `saleor/app/manifest_validations.py`
Content:
```
1 import logging
2 from collections import defaultdict
3 from typing import Dict, Iterable, List
4 
5 from django.contrib.auth.models import Permission
6 from django.core.exceptions import ValidationError
7 from django.db.models import Value
8 from django.db.models.functions import Concat
9 
10 from ..core.permissions import (
11     get_permissions,
12     get_permissions_enum_list,
13     split_permission_codename,
14 )
15 from ..graphql.core.utils import str_to_enum
16 from ..graphql.webhook.subscription_payload import validate_subscription_query
17 from ..webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType
18 from .error_codes import AppErrorCode
19 from .types import AppExtensionMount, AppExtensionTarget
20 from .validators import AppURLValidator
21 
22 logger = logging.getLogger(__name__)
23 
24 T_ERRORS = Dict[str, List[ValidationError]]
25 
26 
27 def _clean_app_url(url):
28     url_validator = AppURLValidator()
29     url_validator(url)
30 
31 
32 def _clean_extension_url_with_only_path(
33     manifest_data: dict, target: str, extension_url: str
34 ):
35     if target == AppExtensionTarget.APP_PAGE:
36         return
37     elif manifest_data["appUrl"]:
38         _clean_app_url(manifest_data["appUrl"])
39     else:
40         msg = (
41             "Incorrect relation between extension's target and URL fields. "
42             "APP_PAGE can be used only with relative URL path."
43         )
44         logger.warning(msg, extra={"target": target, "url": extension_url})
45         raise ValidationError(msg)
46 
47 
48 def clean_extension_url(extension: dict, manifest_data: dict):
49     """Clean assigned extension url.
50 
51     Make sure that format of url is correct based on the rest of manifest fields.
52     - url can start with '/' when one of these conditions is true:
53         a) extension.target == APP_PAGE
54         b) appUrl is provided
55     - url cannot start with protocol when target == "APP_PAGE"
56     """
57     extension_url = extension["url"]
58     target = extension.get("target") or AppExtensionTarget.POPUP
59     if extension_url.startswith("/"):
60         _clean_extension_url_with_only_path(manifest_data, target, extension_url)
61     elif target == AppExtensionTarget.APP_PAGE:
62         msg = "Url cannot start with protocol when target == APP_PAGE"
63         logger.warning(msg)
64         raise ValidationError(msg)
65     else:
66         _clean_app_url(extension_url)
67 
68 
69 def clean_manifest_url(manifest_url):
70     try:
71         _clean_app_url(manifest_url)
72     except (ValidationError, AttributeError):
73         msg = "Enter a valid URL."
74         code = AppErrorCode.INVALID_URL_FORMAT.value
75         raise ValidationError({"manifest_url": ValidationError(msg, code=code)})
76 
77 
78 def clean_permissions(
79     required_permissions: List[str], saleor_permissions: Iterable[Permission]
80 ) -> List[Permission]:
81     missing_permissions = []
82     all_permissions = {perm[0]: perm[1] for perm in get_permissions_enum_list()}
83     for perm in required_permissions:
84         if not all_permissions.get(perm):
85             missing_permissions.append(perm)
86     if missing_permissions:
87         error_msg = "Given permissions don't exist."
88         code = AppErrorCode.INVALID_PERMISSION.value
89         params = {"permissions": missing_permissions}
90         raise ValidationError(error_msg, code=code, params=params)
91 
92     permissions = [all_permissions[perm] for perm in required_permissions]
93     permissions = split_permission_codename(permissions)
94     return [p for p in saleor_permissions if p.codename in permissions]
95 
96 
97 def clean_manifest_data(manifest_data):
98     errors: T_ERRORS = defaultdict(list)
99 
100     validate_required_fields(manifest_data, errors)
101     try:
102         _clean_app_url(manifest_data["tokenTargetUrl"])
103     except (ValidationError, AttributeError):
104         errors["tokenTargetUrl"].append(
105             ValidationError(
106                 "Incorrect format.",
107                 code=AppErrorCode.INVALID_URL_FORMAT.value,
108             )
109         )
110 
111     saleor_permissions = get_permissions().annotate(
112         formated_codename=Concat("content_type__app_label", Value("."), "codename")
113     )
114     try:
115         app_permissions = clean_permissions(
116             manifest_data.get("permissions", []), saleor_permissions
117         )
118     except ValidationError as e:
119         errors["permissions"].append(e)
120         app_permissions = []
121 
122     manifest_data["permissions"] = app_permissions
123 
124     if not errors:
125         clean_extensions(manifest_data, app_permissions, errors)
126         clean_webhooks(manifest_data, errors)
127 
128     if errors:
129         raise ValidationError(errors)
130 
131 
132 def _clean_extension_permissions(extension, app_permissions, errors):
133     permissions_data = extension.get("permissions", [])
134     try:
135         extension_permissions = clean_permissions(permissions_data, app_permissions)
136     except ValidationError as e:
137         e.params["label"] = extension.get("label")
138         errors["extensions"].append(e)
139         return
140 
141     if len(extension_permissions) != len(permissions_data):
142         errors["extensions"].append(
143             ValidationError(
144                 "Extension permission must be listed in App's permissions.",
145                 code=AppErrorCode.OUT_OF_SCOPE_PERMISSION.value,
146             )
147         )
148 
149     extension["permissions"] = extension_permissions
150 
151 
152 def clean_extension_enum_field(enum, field_name, extension, errors):
153     if extension[field_name] in [code.upper() for code, _ in enum.CHOICES]:
154         extension[field_name] = getattr(enum, extension[field_name])
155     else:
156         errors["extensions"].append(
157             ValidationError(
158                 f"Incorrect value for field: {field_name}",
159                 code=AppErrorCode.INVALID.value,
160             )
161         )
162 
163 
164 def clean_extensions(manifest_data, app_permissions, errors):
165     extensions = manifest_data.get("extensions", [])
166     for extension in extensions:
167         if "target" not in extension:
168             extension["target"] = AppExtensionTarget.POPUP
169         else:
170             clean_extension_enum_field(AppExtensionTarget, "target", extension, errors)
171         clean_extension_enum_field(AppExtensionMount, "mount", extension, errors)
172 
173         try:
174             clean_extension_url(extension, manifest_data)
175         except (ValidationError, AttributeError):
176             errors["extensions"].append(
177                 ValidationError(
178                     "Incorrect value for field: url.",
179                     code=AppErrorCode.INVALID_URL_FORMAT.value,
180                 )
181             )
182         _clean_extension_permissions(extension, app_permissions, errors)
183 
184 
185 def clean_webhooks(manifest_data, errors):
186     webhooks = manifest_data.get("webhooks", [])
187 
188     async_types = {
189         str_to_enum(e_type[0]): e_type[0] for e_type in WebhookEventAsyncType.CHOICES
190     }
191     sync_types = {
192         str_to_enum(e_type[0]): e_type[0] for e_type in WebhookEventSyncType.CHOICES
193     }
194 
195     target_url_validator = AppURLValidator(
196         schemes=["http", "https", "awssqs", "gcpubsub"]
197     )
198 
199     for webhook in webhooks:
200         if not validate_subscription_query(webhook["query"]):
201             errors["webhooks"].append(
202                 ValidationError(
203                     "Subscription query is not valid.",
204                     code=AppErrorCode.INVALID.value,
205                 )
206             )
207 
208         webhook["events"] = []
209         for e_type in webhook.get("asyncEvents", []):
210             try:
211                 webhook["events"].append(async_types[e_type])
212             except KeyError:
213                 errors["webhooks"].append(
214                     ValidationError(
215                         "Invalid asynchronous event.",
216                         code=AppErrorCode.INVALID.value,
217                     )
218                 )
219         for e_type in webhook.get("syncEvents", []):
220             try:
221                 webhook["events"].append(sync_types[e_type])
222             except KeyError:
223                 errors["webhooks"].append(
224                     ValidationError(
225                         "Invalid synchronous event.",
226                         code=AppErrorCode.INVALID.value,
227                     )
228                 )
229 
230         try:
231             target_url_validator(webhook["targetUrl"])
232         except ValidationError:
233             errors["webhooks"].append(
234                 ValidationError(
235                     "Invalid target url.",
236                     code=AppErrorCode.INVALID_URL_FORMAT.value,
237                 )
238             )
239 
240 
241 def validate_required_fields(manifest_data, errors):
242     manifest_required_fields = {"id", "version", "name", "tokenTargetUrl"}
243     extension_required_fields = {"label", "url", "mount"}
244     webhook_required_fields = {"name", "targetUrl", "query"}
245 
246     if manifest_missing_fields := manifest_required_fields.difference(manifest_data):
247         for missing_field in manifest_missing_fields:
248             errors[missing_field].append(
249                 ValidationError("Field required.", code=AppErrorCode.REQUIRED.value)
250             )
251 
252     app_extensions_data = manifest_data.get("extensions", [])
253     for extension in app_extensions_data:
254         extension_fields = set(extension.keys())
255         if missing_fields := extension_required_fields.difference(extension_fields):
256             errors["extensions"].append(
257                 ValidationError(
258                     "Missing required fields for app extension: "
259                     f'{", ".join(missing_fields)}.',
260                     code=AppErrorCode.REQUIRED.value,
261                 )
262             )
263 
264     webhooks = manifest_data.get("webhooks", [])
265     for webhook in webhooks:
266         webhook_fields = set(webhook.keys())
267         if missing_fields := webhook_required_fields.difference(webhook_fields):
268             errors["webhooks"].append(
269                 ValidationError(
270                     f"Missing required fields for webhook: "
271                     f'{", ".join(missing_fields)}.',
272                     code=AppErrorCode.REQUIRED.value,
273                 )
274             )
275
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/saleor/app/manifest_validations.py b/saleor/app/manifest_validations.py
--- a/saleor/app/manifest_validations.py
+++ b/saleor/app/manifest_validations.py
@@ -99,7 +99,8 @@
 
     validate_required_fields(manifest_data, errors)
     try:
-        _clean_app_url(manifest_data["tokenTargetUrl"])
+        if "tokenTargetUrl" in manifest_data:
+            _clean_app_url(manifest_data["tokenTargetUrl"])
     except (ValidationError, AttributeError):
         errors["tokenTargetUrl"].append(
             ValidationError(

{"golden_diff": "diff --git a/saleor/app/manifest_validations.py b/saleor/app/manifest_validations.py\n--- a/saleor/app/manifest_validations.py\n+++ b/saleor/app/manifest_validations.py\n@@ -99,7 +99,8 @@\n \n     validate_required_fields(manifest_data, errors)\n     try:\n-        _clean_app_url(manifest_data[\"tokenTargetUrl\"])\n+        if \"tokenTargetUrl\" in manifest_data:\n+            _clean_app_url(manifest_data[\"tokenTargetUrl\"])\n     except (ValidationError, AttributeError):\n         errors[\"tokenTargetUrl\"].append(\n             ValidationError(\n", "issue": "Bug: Lack of validation in cleaning manifest data\n### What are you trying to achieve?\n\nThe `KeyError` is raised when `manifest_data` doesn't have `tokenTargetUrl`.\n\n### Steps to reproduce the problem\n\n1. Run `AppFetchManifest` with URL that contains JSON data without `tokenTargetUrl`.\r\n2. You will get the `KeyError`.\n\n### What did you expect to happen?\n\nThe `ValidationError` should be raised.\n\n### Logs\n\nhttps://sentry.io/organizations/saleor/issues/3749157627/?project=6417854\n\n### Environment\n\nSaleor version: 3.9 (to check if it also affect other versions)\n", "before_files": [{"content": "import logging\nfrom collections import defaultdict\nfrom typing import Dict, Iterable, List\n\nfrom django.contrib.auth.models import Permission\nfrom django.core.exceptions import ValidationError\nfrom django.db.models import Value\nfrom django.db.models.functions import Concat\n\nfrom ..core.permissions import (\n    get_permissions,\n    get_permissions_enum_list,\n    split_permission_codename,\n)\nfrom ..graphql.core.utils import str_to_enum\nfrom ..graphql.webhook.subscription_payload import validate_subscription_query\nfrom ..webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType\nfrom .error_codes import AppErrorCode\nfrom .types import AppExtensionMount, AppExtensionTarget\nfrom .validators import AppURLValidator\n\nlogger = logging.getLogger(__name__)\n\nT_ERRORS = Dict[str, List[ValidationError]]\n\n\ndef _clean_app_url(url):\n    url_validator = AppURLValidator()\n    url_validator(url)\n\n\ndef _clean_extension_url_with_only_path(\n    manifest_data: dict, target: str, extension_url: str\n):\n    if target == AppExtensionTarget.APP_PAGE:\n        return\n    elif manifest_data[\"appUrl\"]:\n        _clean_app_url(manifest_data[\"appUrl\"])\n    else:\n        msg = (\n            \"Incorrect relation between extension's target and URL fields. \"\n            \"APP_PAGE can be used only with relative URL path.\"\n        )\n        logger.warning(msg, extra={\"target\": target, \"url\": extension_url})\n        raise ValidationError(msg)\n\n\ndef clean_extension_url(extension: dict, manifest_data: dict):\n    \"\"\"Clean assigned extension url.\n\n    Make sure that format of url is correct based on the rest of manifest fields.\n    - url can start with '/' when one of these conditions is true:\n        a) extension.target == APP_PAGE\n        b) appUrl is provided\n    - url cannot start with protocol when target == \"APP_PAGE\"\n    \"\"\"\n    extension_url = extension[\"url\"]\n    target = extension.get(\"target\") or AppExtensionTarget.POPUP\n    if extension_url.startswith(\"/\"):\n        _clean_extension_url_with_only_path(manifest_data, target, extension_url)\n    elif target == AppExtensionTarget.APP_PAGE:\n        msg = \"Url cannot start with protocol when target == APP_PAGE\"\n        logger.warning(msg)\n        raise ValidationError(msg)\n    else:\n        _clean_app_url(extension_url)\n\n\ndef clean_manifest_url(manifest_url):\n    try:\n        _clean_app_url(manifest_url)\n    except (ValidationError, AttributeError):\n        msg = \"Enter a valid URL.\"\n        code = AppErrorCode.INVALID_URL_FORMAT.value\n        raise ValidationError({\"manifest_url\": ValidationError(msg, code=code)})\n\n\ndef clean_permissions(\n    required_permissions: List[str], saleor_permissions: Iterable[Permission]\n) -> List[Permission]:\n    missing_permissions = []\n    all_permissions = {perm[0]: perm[1] for perm in get_permissions_enum_list()}\n    for perm in required_permissions:\n        if not all_permissions.get(perm):\n            missing_permissions.append(perm)\n    if missing_permissions:\n        error_msg = \"Given permissions don't exist.\"\n        code = AppErrorCode.INVALID_PERMISSION.value\n        params = {\"permissions\": missing_permissions}\n        raise ValidationError(error_msg, code=code, params=params)\n\n    permissions = [all_permissions[perm] for perm in required_permissions]\n    permissions = split_permission_codename(permissions)\n    return [p for p in saleor_permissions if p.codename in permissions]\n\n\ndef clean_manifest_data(manifest_data):\n    errors: T_ERRORS = defaultdict(list)\n\n    validate_required_fields(manifest_data, errors)\n    try:\n        _clean_app_url(manifest_data[\"tokenTargetUrl\"])\n    except (ValidationError, AttributeError):\n        errors[\"tokenTargetUrl\"].append(\n            ValidationError(\n                \"Incorrect format.\",\n                code=AppErrorCode.INVALID_URL_FORMAT.value,\n            )\n        )\n\n    saleor_permissions = get_permissions().annotate(\n        formated_codename=Concat(\"content_type__app_label\", Value(\".\"), \"codename\")\n    )\n    try:\n        app_permissions = clean_permissions(\n            manifest_data.get(\"permissions\", []), saleor_permissions\n        )\n    except ValidationError as e:\n        errors[\"permissions\"].append(e)\n        app_permissions = []\n\n    manifest_data[\"permissions\"] = app_permissions\n\n    if not errors:\n        clean_extensions(manifest_data, app_permissions, errors)\n        clean_webhooks(manifest_data, errors)\n\n    if errors:\n        raise ValidationError(errors)\n\n\ndef _clean_extension_permissions(extension, app_permissions, errors):\n    permissions_data = extension.get(\"permissions\", [])\n    try:\n        extension_permissions = clean_permissions(permissions_data, app_permissions)\n    except ValidationError as e:\n        e.params[\"label\"] = extension.get(\"label\")\n        errors[\"extensions\"].append(e)\n        return\n\n    if len(extension_permissions) != len(permissions_data):\n        errors[\"extensions\"].append(\n            ValidationError(\n                \"Extension permission must be listed in App's permissions.\",\n                code=AppErrorCode.OUT_OF_SCOPE_PERMISSION.value,\n            )\n        )\n\n    extension[\"permissions\"] = extension_permissions\n\n\ndef clean_extension_enum_field(enum, field_name, extension, errors):\n    if extension[field_name] in [code.upper() for code, _ in enum.CHOICES]:\n        extension[field_name] = getattr(enum, extension[field_name])\n    else:\n        errors[\"extensions\"].append(\n            ValidationError(\n                f\"Incorrect value for field: {field_name}\",\n                code=AppErrorCode.INVALID.value,\n            )\n        )\n\n\ndef clean_extensions(manifest_data, app_permissions, errors):\n    extensions = manifest_data.get(\"extensions\", [])\n    for extension in extensions:\n        if \"target\" not in extension:\n            extension[\"target\"] = AppExtensionTarget.POPUP\n        else:\n            clean_extension_enum_field(AppExtensionTarget, \"target\", extension, errors)\n        clean_extension_enum_field(AppExtensionMount, \"mount\", extension, errors)\n\n        try:\n            clean_extension_url(extension, manifest_data)\n        except (ValidationError, AttributeError):\n            errors[\"extensions\"].append(\n                ValidationError(\n                    \"Incorrect value for field: url.\",\n                    code=AppErrorCode.INVALID_URL_FORMAT.value,\n                )\n            )\n        _clean_extension_permissions(extension, app_permissions, errors)\n\n\ndef clean_webhooks(manifest_data, errors):\n    webhooks = manifest_data.get(\"webhooks\", [])\n\n    async_types = {\n        str_to_enum(e_type[0]): e_type[0] for e_type in WebhookEventAsyncType.CHOICES\n    }\n    sync_types = {\n        str_to_enum(e_type[0]): e_type[0] for e_type in WebhookEventSyncType.CHOICES\n    }\n\n    target_url_validator = AppURLValidator(\n        schemes=[\"http\", \"https\", \"awssqs\", \"gcpubsub\"]\n    )\n\n    for webhook in webhooks:\n        if not validate_subscription_query(webhook[\"query\"]):\n            errors[\"webhooks\"].append(\n                ValidationError(\n                    \"Subscription query is not valid.\",\n                    code=AppErrorCode.INVALID.value,\n                )\n            )\n\n        webhook[\"events\"] = []\n        for e_type in webhook.get(\"asyncEvents\", []):\n            try:\n                webhook[\"events\"].append(async_types[e_type])\n            except KeyError:\n                errors[\"webhooks\"].append(\n                    ValidationError(\n                        \"Invalid asynchronous event.\",\n                        code=AppErrorCode.INVALID.value,\n                    )\n                )\n        for e_type in webhook.get(\"syncEvents\", []):\n            try:\n                webhook[\"events\"].append(sync_types[e_type])\n            except KeyError:\n                errors[\"webhooks\"].append(\n                    ValidationError(\n                        \"Invalid synchronous event.\",\n                        code=AppErrorCode.INVALID.value,\n                    )\n                )\n\n        try:\n            target_url_validator(webhook[\"targetUrl\"])\n        except ValidationError:\n            errors[\"webhooks\"].append(\n                ValidationError(\n                    \"Invalid target url.\",\n                    code=AppErrorCode.INVALID_URL_FORMAT.value,\n                )\n            )\n\n\ndef validate_required_fields(manifest_data, errors):\n    manifest_required_fields = {\"id\", \"version\", \"name\", \"tokenTargetUrl\"}\n    extension_required_fields = {\"label\", \"url\", \"mount\"}\n    webhook_required_fields = {\"name\", \"targetUrl\", \"query\"}\n\n    if manifest_missing_fields := manifest_required_fields.difference(manifest_data):\n        for missing_field in manifest_missing_fields:\n            errors[missing_field].append(\n                ValidationError(\"Field required.\", code=AppErrorCode.REQUIRED.value)\n            )\n\n    app_extensions_data = manifest_data.get(\"extensions\", [])\n    for extension in app_extensions_data:\n        extension_fields = set(extension.keys())\n        if missing_fields := extension_required_fields.difference(extension_fields):\n            errors[\"extensions\"].append(\n                ValidationError(\n                    \"Missing required fields for app extension: \"\n                    f'{\", \".join(missing_fields)}.',\n                    code=AppErrorCode.REQUIRED.value,\n                )\n            )\n\n    webhooks = manifest_data.get(\"webhooks\", [])\n    for webhook in webhooks:\n        webhook_fields = set(webhook.keys())\n        if missing_fields := webhook_required_fields.difference(webhook_fields):\n            errors[\"webhooks\"].append(\n                ValidationError(\n                    f\"Missing required fields for webhook: \"\n                    f'{\", \".join(missing_fields)}.',\n                    code=AppErrorCode.REQUIRED.value,\n                )\n            )\n", "path": "saleor/app/manifest_validations.py"}], "after_files": [{"content": "import logging\nfrom collections import defaultdict\nfrom typing import Dict, Iterable, List\n\nfrom django.contrib.auth.models import Permission\nfrom django.core.exceptions import ValidationError\nfrom django.db.models import Value\nfrom django.db.models.functions import Concat\n\nfrom ..core.permissions import (\n    get_permissions,\n    get_permissions_enum_list,\n    split_permission_codename,\n)\nfrom ..graphql.core.utils import str_to_enum\nfrom ..graphql.webhook.subscription_payload import validate_subscription_query\nfrom ..webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType\nfrom .error_codes import AppErrorCode\nfrom .types import AppExtensionMount, AppExtensionTarget\nfrom .validators import AppURLValidator\n\nlogger = logging.getLogger(__name__)\n\nT_ERRORS = Dict[str, List[ValidationError]]\n\n\ndef _clean_app_url(url):\n    url_validator = AppURLValidator()\n    url_validator(url)\n\n\ndef _clean_extension_url_with_only_path(\n    manifest_data: dict, target: str, extension_url: str\n):\n    if target == AppExtensionTarget.APP_PAGE:\n        return\n    elif manifest_data[\"appUrl\"]:\n        _clean_app_url(manifest_data[\"appUrl\"])\n    else:\n        msg = (\n            \"Incorrect relation between extension's target and URL fields. \"\n            \"APP_PAGE can be used only with relative URL path.\"\n        )\n        logger.warning(msg, extra={\"target\": target, \"url\": extension_url})\n        raise ValidationError(msg)\n\n\ndef clean_extension_url(extension: dict, manifest_data: dict):\n    \"\"\"Clean assigned extension url.\n\n    Make sure that format of url is correct based on the rest of manifest fields.\n    - url can start with '/' when one of these conditions is true:\n        a) extension.target == APP_PAGE\n        b) appUrl is provided\n    - url cannot start with protocol when target == \"APP_PAGE\"\n    \"\"\"\n    extension_url = extension[\"url\"]\n    target = extension.get(\"target\") or AppExtensionTarget.POPUP\n    if extension_url.startswith(\"/\"):\n        _clean_extension_url_with_only_path(manifest_data, target, extension_url)\n    elif target == AppExtensionTarget.APP_PAGE:\n        msg = \"Url cannot start with protocol when target == APP_PAGE\"\n        logger.warning(msg)\n        raise ValidationError(msg)\n    else:\n        _clean_app_url(extension_url)\n\n\ndef clean_manifest_url(manifest_url):\n    try:\n        _clean_app_url(manifest_url)\n    except (ValidationError, AttributeError):\n        msg = \"Enter a valid URL.\"\n        code = AppErrorCode.INVALID_URL_FORMAT.value\n        raise ValidationError({\"manifest_url\": ValidationError(msg, code=code)})\n\n\ndef clean_permissions(\n    required_permissions: List[str], saleor_permissions: Iterable[Permission]\n) -> List[Permission]:\n    missing_permissions = []\n    all_permissions = {perm[0]: perm[1] for perm in get_permissions_enum_list()}\n    for perm in required_permissions:\n        if not all_permissions.get(perm):\n            missing_permissions.append(perm)\n    if missing_permissions:\n        error_msg = \"Given permissions don't exist.\"\n        code = AppErrorCode.INVALID_PERMISSION.value\n        params = {\"permissions\": missing_permissions}\n        raise ValidationError(error_msg, code=code, params=params)\n\n    permissions = [all_permissions[perm] for perm in required_permissions]\n    permissions = split_permission_codename(permissions)\n    return [p for p in saleor_permissions if p.codename in permissions]\n\n\ndef clean_manifest_data(manifest_data):\n    errors: T_ERRORS = defaultdict(list)\n\n    validate_required_fields(manifest_data, errors)\n    try:\n        if \"tokenTargetUrl\" in manifest_data:\n            _clean_app_url(manifest_data[\"tokenTargetUrl\"])\n    except (ValidationError, AttributeError):\n        errors[\"tokenTargetUrl\"].append(\n            ValidationError(\n                \"Incorrect format.\",\n                code=AppErrorCode.INVALID_URL_FORMAT.value,\n            )\n        )\n\n    saleor_permissions = get_permissions().annotate(\n        formated_codename=Concat(\"content_type__app_label\", Value(\".\"), \"codename\")\n    )\n    try:\n        app_permissions = clean_permissions(\n            manifest_data.get(\"permissions\", []), saleor_permissions\n        )\n    except ValidationError as e:\n        errors[\"permissions\"].append(e)\n        app_permissions = []\n\n    manifest_data[\"permissions\"] = app_permissions\n\n    if not errors:\n        clean_extensions(manifest_data, app_permissions, errors)\n        clean_webhooks(manifest_data, errors)\n\n    if errors:\n        raise ValidationError(errors)\n\n\ndef _clean_extension_permissions(extension, app_permissions, errors):\n    permissions_data = extension.get(\"permissions\", [])\n    try:\n        extension_permissions = clean_permissions(permissions_data, app_permissions)\n    except ValidationError as e:\n        e.params[\"label\"] = extension.get(\"label\")\n        errors[\"extensions\"].append(e)\n        return\n\n    if len(extension_permissions) != len(permissions_data):\n        errors[\"extensions\"].append(\n            ValidationError(\n                \"Extension permission must be listed in App's permissions.\",\n                code=AppErrorCode.OUT_OF_SCOPE_PERMISSION.value,\n            )\n        )\n\n    extension[\"permissions\"] = extension_permissions\n\n\ndef clean_extension_enum_field(enum, field_name, extension, errors):\n    if extension[field_name] in [code.upper() for code, _ in enum.CHOICES]:\n        extension[field_name] = getattr(enum, extension[field_name])\n    else:\n        errors[\"extensions\"].append(\n            ValidationError(\n                f\"Incorrect value for field: {field_name}\",\n                code=AppErrorCode.INVALID.value,\n            )\n        )\n\n\ndef clean_extensions(manifest_data, app_permissions, errors):\n    extensions = manifest_data.get(\"extensions\", [])\n    for extension in extensions:\n        if \"target\" not in extension:\n            extension[\"target\"] = AppExtensionTarget.POPUP\n        else:\n            clean_extension_enum_field(AppExtensionTarget, \"target\", extension, errors)\n        clean_extension_enum_field(AppExtensionMount, \"mount\", extension, errors)\n\n        try:\n            clean_extension_url(extension, manifest_data)\n        except (ValidationError, AttributeError):\n            errors[\"extensions\"].append(\n                ValidationError(\n                    \"Incorrect value for field: url.\",\n                    code=AppErrorCode.INVALID_URL_FORMAT.value,\n                )\n            )\n        _clean_extension_permissions(extension, app_permissions, errors)\n\n\ndef clean_webhooks(manifest_data, errors):\n    webhooks = manifest_data.get(\"webhooks\", [])\n\n    async_types = {\n        str_to_enum(e_type[0]): e_type[0] for e_type in WebhookEventAsyncType.CHOICES\n    }\n    sync_types = {\n        str_to_enum(e_type[0]): e_type[0] for e_type in WebhookEventSyncType.CHOICES\n    }\n\n    target_url_validator = AppURLValidator(\n        schemes=[\"http\", \"https\", \"awssqs\", \"gcpubsub\"]\n    )\n\n    for webhook in webhooks:\n        if not validate_subscription_query(webhook[\"query\"]):\n            errors[\"webhooks\"].append(\n                ValidationError(\n                    \"Subscription query is not valid.\",\n                    code=AppErrorCode.INVALID.value,\n                )\n            )\n\n        webhook[\"events\"] = []\n        for e_type in webhook.get(\"asyncEvents\", []):\n            try:\n                webhook[\"events\"].append(async_types[e_type])\n            except KeyError:\n                errors[\"webhooks\"].append(\n                    ValidationError(\n                        \"Invalid asynchronous event.\",\n                        code=AppErrorCode.INVALID.value,\n                    )\n                )\n        for e_type in webhook.get(\"syncEvents\", []):\n            try:\n                webhook[\"events\"].append(sync_types[e_type])\n            except KeyError:\n                errors[\"webhooks\"].append(\n                    ValidationError(\n                        \"Invalid synchronous event.\",\n                        code=AppErrorCode.INVALID.value,\n                    )\n                )\n\n        try:\n            target_url_validator(webhook[\"targetUrl\"])\n        except ValidationError:\n            errors[\"webhooks\"].append(\n                ValidationError(\n                    \"Invalid target url.\",\n                    code=AppErrorCode.INVALID_URL_FORMAT.value,\n                )\n            )\n\n\ndef validate_required_fields(manifest_data, errors):\n    manifest_required_fields = {\"id\", \"version\", \"name\", \"tokenTargetUrl\"}\n    extension_required_fields = {\"label\", \"url\", \"mount\"}\n    webhook_required_fields = {\"name\", \"targetUrl\", \"query\"}\n\n    if manifest_missing_fields := manifest_required_fields.difference(manifest_data):\n        for missing_field in manifest_missing_fields:\n            errors[missing_field].append(\n                ValidationError(\"Field required.\", code=AppErrorCode.REQUIRED.value)\n            )\n\n    app_extensions_data = manifest_data.get(\"extensions\", [])\n    for extension in app_extensions_data:\n        extension_fields = set(extension.keys())\n        if missing_fields := extension_required_fields.difference(extension_fields):\n            errors[\"extensions\"].append(\n                ValidationError(\n                    \"Missing required fields for app extension: \"\n                    f'{\", \".join(missing_fields)}.',\n                    code=AppErrorCode.REQUIRED.value,\n                )\n            )\n\n    webhooks = manifest_data.get(\"webhooks\", [])\n    for webhook in webhooks:\n        webhook_fields = set(webhook.keys())\n        if missing_fields := webhook_required_fields.difference(webhook_fields):\n            errors[\"webhooks\"].append(\n                ValidationError(\n                    f\"Missing required fields for webhook: \"\n                    f'{\", \".join(missing_fields)}.',\n                    code=AppErrorCode.REQUIRED.value,\n                )\n            )\n", "path": "saleor/app/manifest_validations.py"}]}