diff --git a/backend/src/baserow/contrib/database/airtable/import_report.py b/backend/src/baserow/contrib/database/airtable/import_report.py index 132684ed22..979377940d 100644 --- a/backend/src/baserow/contrib/database/airtable/import_report.py +++ b/backend/src/baserow/contrib/database/airtable/import_report.py @@ -102,7 +102,7 @@ def get_baserow_export_table(self, order: int) -> dict: empty_serialized_grid_view = grid_view_type.export_serialized( grid_view, ImportExportConfig(include_permission_data=False), - None, + {}, None, None, ) diff --git a/backend/src/baserow/contrib/database/airtable/registry.py b/backend/src/baserow/contrib/database/airtable/registry.py index 2c9248d22f..70e69c9687 100644 --- a/backend/src/baserow/contrib/database/airtable/registry.py +++ b/backend/src/baserow/contrib/database/airtable/registry.py @@ -811,7 +811,7 @@ def to_serialized_baserow_view( config, import_report, ) - serialized = view_type.export_serialized(view, config) + serialized = view_type.export_serialized(view, config, {}) return serialized diff --git a/backend/src/baserow/contrib/database/application_types.py b/backend/src/baserow/contrib/database/application_types.py index 18ef724015..dccd8774a1 100755 --- a/backend/src/baserow/contrib/database/application_types.py +++ b/backend/src/baserow/contrib/database/application_types.py @@ -123,12 +123,16 @@ def export_tables_serialized( for table in tables: fields = table.field_set.all() serialized_fields = [] + specific_fields = [] for f in fields: field = f.specific + specific_fields.append(field) field_type = field_type_registry.get_by_model(field) serialized_fields.append(field_type.export_serialized(field)) - table_cache: Dict[str, Any] = {} + table_cache: Dict[str, Any] = { + f"fields_by_id_{table.id}": {f.id: f for f in specific_fields}, + } workspace = table.get_root() if workspace is not None: table_cache["workspace_id"] = workspace.id @@ -939,10 +943,16 @@ def _import_table_views( table = serialized_table["_object"] table_name = serialized_table["name"] + cache: Dict[str, Any] = {} for serialized_view in serialized_table["views"]: view_type = view_type_registry.get(serialized_view["type"]) view_type.import_serialized( - table, serialized_view, import_export_config, id_mapping, files_zip + table, + serialized_view, + import_export_config, + id_mapping, + cache, + files_zip, ) progress.increment( state=f"{IMPORT_SERIALIZED_IMPORTING_TABLE_STRUCTURE}{table_name}" diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py index d28d313ad6..7fd76e893d 100755 --- a/backend/src/baserow/contrib/database/fields/field_types.py +++ b/backend/src/baserow/contrib/database/fields/field_types.py @@ -4395,7 +4395,7 @@ def get_internal_value_from_db( ) -> int: return getattr(row, f"{field_name}_id") - def import_serialized_default_value(self, value, id_mapping): + def import_serialized_default_value(self, value, id_mapping, workspace_id, cache): if isinstance(value, int): option_mapping = id_mapping.get("database_field_select_options", {}) return option_mapping.get(value, value) @@ -4768,7 +4768,7 @@ class MultipleSelectFieldType( ), } - def import_serialized_default_value(self, value, id_mapping): + def import_serialized_default_value(self, value, id_mapping, workspace_id, cache): if isinstance(value, list): option_mapping = id_mapping.get("database_field_select_options", {}) return [ @@ -7057,6 +7057,50 @@ def set_import_serialized_value( return through_objects + def export_serialized_default_value(self, value, field, workspace_id, cache): + if not isinstance(value, list): + return value + + cache_entry = f"collaborator_id_to_email_export_{workspace_id}" + if cache_entry not in cache: + cache[cache_entry] = dict( + WorkspaceUser.objects.filter(workspace_id=workspace_id).values_list( + "user_id", "user__email" + ) + ) + + id_to_email = cache[cache_entry] + return [ + id_to_email[user["id"]] + for user in value + if isinstance(user["id"], int) and user["id"] in id_to_email + ] + + def import_serialized_default_value(self, value, id_mapping, workspace_id, cache): + if not isinstance(value, list): + return value + + cache_key = f"collaborator_email_to_id_import_{workspace_id}" + if cache_key not in cache: + cache[cache_key] = { + workspace_user["user__email"]: workspace_user + for workspace_user in WorkspaceUser.objects.filter( + workspace_id=workspace_id + ) + .select_related("user") + .values("user__email", "user__first_name", "user_id") + } + + email_to_id = cache[cache_key] + return [ + { + "id": email_to_id[email]["user_id"], + "name": email_to_id[email]["user__first_name"], + } + for email in value + if isinstance(email, str) and email in email_to_id + ] + def random_value(self, instance, fake, cache): """ Selects a random sublist out of the possible collaborators. diff --git a/backend/src/baserow/contrib/database/fields/registries.py b/backend/src/baserow/contrib/database/fields/registries.py index ae8673d05b..5d1ab99d0f 100644 --- a/backend/src/baserow/contrib/database/fields/registries.py +++ b/backend/src/baserow/contrib/database/fields/registries.py @@ -1253,10 +1253,34 @@ def set_import_serialized_value( setattr(row, field_name, value) + def export_serialized_default_value( + self, + value: Any, + field: "Field", + workspace_id: int, + cache: Dict, + ) -> Any: + """ + Hook that is called when exporting a ViewDefaultValue.value during a + serialized export. Can be used to convert internal IDs to portable + representations (e.g. user IDs to email addresses). + + :param value: The raw JSON default value to export. + :param field: The field instance the default value belongs to. + :param workspace_id: The ID of the workspace being exported. + :param cache: A cache dict shared across the export to avoid repeated + queries. + :return: The portable representation of the value. + """ + + return value + def import_serialized_default_value( self, value: Any, id_mapping: Dict[str, Any], + workspace_id: int, + cache: Dict, ) -> Any: """ Hook that is called just before the ViewDefaultValue.value is set when doing a @@ -1265,6 +1289,9 @@ def import_serialized_default_value( :param value: The raw JSON default value to remap. :param id_mapping: The map of exported ids to newly created ids. + :param workspace_id: The ID of the workspace being imported into. + :param cache: A cache dict shared across the import to avoid repeated + queries. :return: The value with remapped IDs. """ diff --git a/backend/src/baserow/contrib/database/views/handler.py b/backend/src/baserow/contrib/database/views/handler.py index ca57089e5d..7407f94212 100644 --- a/backend/src/baserow/contrib/database/views/handler.py +++ b/backend/src/baserow/contrib/database/views/handler.py @@ -1065,7 +1065,7 @@ def duplicate_view(self, user: AbstractUser, original_view: View) -> View: "database_field_select_options": MirrorDict(), } duplicated_view = view_type.import_serialized( - original_view.table, serialized, config, id_mapping + original_view.table, serialized, config, id_mapping, {} ) if duplicated_view is None: diff --git a/backend/src/baserow/contrib/database/views/registries.py b/backend/src/baserow/contrib/database/views/registries.py index 0af49dcfdc..46b3b7924b 100644 --- a/backend/src/baserow/contrib/database/views/registries.py +++ b/backend/src/baserow/contrib/database/views/registries.py @@ -22,6 +22,7 @@ from rest_framework.serializers import Serializer from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ +from baserow.core.db import specific_iterator from baserow.core.exceptions import PermissionDenied from baserow.core.handler import CoreHandler from baserow.core.models import Workspace, WorkspaceUser @@ -230,7 +231,7 @@ def export_serialized( self, view: "View", import_export_config: ImportExportConfig, - cache: Optional[Dict] = None, + cache: Dict, files_zip: Optional[ExportZipFile] = None, storage: Optional[Storage] = None, ) -> Dict[str, Any]: @@ -338,6 +339,7 @@ def import_serialized( serialized_values: Dict[str, Any], import_export_config: ImportExportConfig, id_mapping: Dict[str, Any], + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ) -> Optional["View"]: @@ -353,6 +355,7 @@ def import_serialized( import/export process to customize how it works. :param id_mapping: The map of exported ids to newly created ids that must be updated when a new instance has been created. + :param cache: A cache to use for storing temporary data. :param files_zip: A zip file buffer where files related to the export can be extracted from. :param storage: The storage where the files can be copied to. @@ -549,6 +552,7 @@ def import_serialized( id_mapping, files_zip, storage, + cache, ) for ( @@ -564,20 +568,44 @@ def _export_default_row_values(self, view, cache, files_zip, storage): """ Exports the default row values for the given view as a serialized dict. Uses the prefetched ``view_default_values`` related manager when - available to avoid extra queries during batch exports. The raw JSON - value is exported as-is since it is already serializable. + available. Field objects are looked up from + ``cache["fields_by_id_{table_id}"]``, which is lazily populated using + ``specific_iterator`` if not already set by the caller. The value is + passed through the field type's ``export_serialized_default_value`` + hook so that internal IDs can be converted to portable representations. """ + from baserow.contrib.database.fields.models import Field + from baserow.contrib.database.fields.registries import field_type_registry + default_values = view.view_default_values.all() if not default_values: - return None + return {} + + workspace_id = view.table.database.workspace_id + table_id = view.table_id + cache_key = f"fields_by_id_{table_id}" + if cache_key not in cache: + cache[cache_key] = { + f.id: f + for f in specific_iterator(Field.objects.filter(table_id=table_id)) + } + fields_by_id = cache[cache_key] serialized_values = {} for default_value in default_values: + value = default_value.value + field = fields_by_id.get(default_value.field_id) + if field is not None and value is not None: + field_type = field_type_registry.get_by_model(field.specific_class) + value = field_type.export_serialized_default_value( + value, field, workspace_id, cache + ) + serialized_values[str(default_value.field_id)] = { "field_id": default_value.field_id, "enabled": default_value.enabled, - "value": default_value.value, + "value": value, "function": default_value.function, "field_type": default_value.field_type, } @@ -592,6 +620,7 @@ def _import_default_row_values( id_mapping, files_zip, storage, + cache, ): """ Imports the default row values from a serialized dict using @@ -603,6 +632,7 @@ def _import_default_row_values( from baserow.contrib.database.views.models import ViewDefaultValue + workspace_id = table.database.workspace_id model = table.get_model() records = [] @@ -621,7 +651,9 @@ def _import_default_row_values( value = default_value_data.get("value") if value is not None: - value = field_type.import_serialized_default_value(value, id_mapping) + value = field_type.import_serialized_default_value( + value, id_mapping, workspace_id, cache + ) records.append( ViewDefaultValue( diff --git a/backend/src/baserow/contrib/database/views/view_types.py b/backend/src/baserow/contrib/database/views/view_types.py index 7d26811ced..7348f2e176 100644 --- a/backend/src/baserow/contrib/database/views/view_types.py +++ b/backend/src/baserow/contrib/database/views/view_types.py @@ -110,7 +110,7 @@ def export_serialized( self, grid: View, import_export_config: ImportExportConfig, - cache: Optional[Dict] = None, + cache: Dict, files_zip: Optional[ExportZipFile] = None, storage: Optional[Storage] = None, ): @@ -147,6 +147,7 @@ def import_serialized( serialized_values: Dict[str, Any], import_export_config: ImportExportConfig, id_mapping: Dict[str, Any], + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ) -> Optional[View]: @@ -157,7 +158,13 @@ def import_serialized( serialized_copy = serialized_values.copy() field_options = serialized_copy.pop("field_options") grid_view = super().import_serialized( - table, serialized_copy, import_export_config, id_mapping, files_zip, storage + table, + serialized_copy, + import_export_config, + id_mapping, + cache, + files_zip, + storage, ) if grid_view is not None: if "database_grid_view_field_options" not in id_mapping: @@ -425,7 +432,7 @@ def export_serialized( self, gallery: View, import_export_config: ImportExportConfig, - cache: Optional[Dict] = None, + cache: Dict, files_zip: Optional[ExportZipFile] = None, storage: Optional[Storage] = None, ): @@ -460,6 +467,7 @@ def import_serialized( serialized_values: Dict[str, Any], import_export_config: ImportExportConfig, id_mapping: Dict[str, Any], + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ) -> Optional[View]: @@ -477,7 +485,13 @@ def import_serialized( field_options = serialized_copy.pop("field_options") gallery_view = super().import_serialized( - table, serialized_copy, import_export_config, id_mapping, files_zip, storage + table, + serialized_copy, + import_export_config, + id_mapping, + cache, + files_zip, + storage, ) if gallery_view is not None: @@ -1121,7 +1135,7 @@ def export_serialized( self, form: View, import_export_config: ImportExportConfig, - cache: Optional[Dict] = None, + cache: Dict, files_zip: Optional[ExportZipFile] = None, storage: Optional[Storage] = None, ): @@ -1210,6 +1224,7 @@ def import_serialized( serialized_values: Dict[str, Any], import_export_config: ImportExportConfig, id_mapping: Dict[str, Any], + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ) -> Optional[View]: @@ -1235,7 +1250,13 @@ def get_file(file): serialized_copy["logo_image"] = get_file(serialized_copy.pop("logo_image")) field_options = serialized_copy.pop("field_options") form_view = super().import_serialized( - table, serialized_copy, import_export_config, id_mapping, files_zip, storage + table, + serialized_copy, + import_export_config, + id_mapping, + cache, + files_zip, + storage, ) if form_view is not None: diff --git a/backend/src/baserow/contrib/integrations/core/service_types.py b/backend/src/baserow/contrib/integrations/core/service_types.py index c296a3e4fc..b8fe0c4893 100644 --- a/backend/src/baserow/contrib/integrations/core/service_types.py +++ b/backend/src/baserow/contrib/integrations/core/service_types.py @@ -724,8 +724,6 @@ def serializer_field_overrides(self): def _instance_smtp_is_available(self) -> bool: return bool( settings.INTEGRATION_ALLOW_SMTP_SERVICE_TO_USE_INSTANCE_SETTINGS - and getattr(settings, "CELERY_EMAIL_BACKEND", None) - == "django.core.mail.backends.smtp.EmailBackend" and getattr(settings, "EMAIL_HOST", "") ) @@ -855,7 +853,7 @@ def dispatch_data( else resolved_values["from_email"] ) connection = get_connection( - backend=settings.CELERY_EMAIL_BACKEND, + backend="django.core.mail.backends.smtp.EmailBackend", host=smtp_integration.host, port=smtp_integration.port, username=smtp_integration.username, diff --git a/backend/src/baserow/core/import_export/handler.py b/backend/src/baserow/core/import_export/handler.py index 15d4a258df..d291870985 100644 --- a/backend/src/baserow/core/import_export/handler.py +++ b/backend/src/baserow/core/import_export/handler.py @@ -758,7 +758,7 @@ def validate_checksums(self, manifest: Dict, import_tmp_dir: str, storage: Stora checksums = manifest["checksums"] for file_path, checksum in checksums.items(): - full_path = join(import_tmp_dir, file_path) + full_path = self._validate_safe_path(import_tmp_dir, file_path) if not storage.exists(full_path): raise ImportExportResourceDoesNotExist( @@ -799,7 +799,9 @@ def import_application( :return: The imported Application instance. """ - data_file_path = join(import_tmp_path, application_manifest["files"]["schema"]) + data_file_path = self._validate_safe_path( + import_tmp_path, application_manifest["files"]["schema"] + ) if not storage.exists(data_file_path): raise ImportExportResourceDoesNotExist( f"The file {data_file_path} does not exist." @@ -935,23 +937,59 @@ def application_priority_sort(application_to_sort): Application.objects.bulk_update(imported_applications, ["order"]) return imported_applications + @staticmethod + def _validate_safe_path(base_path: str, filename: str) -> str: + """ + Validates that a filename, when joined with base_path, does not escape + the base directory via path traversal sequences. + + :param base_path: The trusted base directory. + :param filename: The untrusted filename from the archive or manifest. + :return: The safe full path (join of base_path and normalized filename). + :raises SuspiciousOperation: If the path would escape the base directory. + """ + + normalized = os.path.normpath(filename) + if normalized.startswith("..") or os.path.isabs(normalized): + raise SuspiciousOperation(f"Detected path traversal attempt: {filename}") + return join(base_path, normalized) + + @staticmethod + def _build_allowed_files(manifest_data: dict) -> list: + """ + Builds the complete list of filenames allowed in an import ZIP archive + from the manifest data. Includes checksummed data files plus the + manifest and signature meta files. + + :param manifest_data: The parsed manifest dictionary. + :return: List of allowed filenames. + """ + + allowed = list(manifest_data.get("checksums", {}).keys()) + allowed.append(MANIFEST_NAME) + allowed.append(SIGNATURE_NAME) + return allowed + def extract_files_from_zip( self, tmp_import_path: str, zip_file: ZipFile, storage: Storage, + allowed_files: list, progress_builder: Optional[ChildProgressBuilder] = None, ): """ Extracts files from a zip archive to a specified temporary import path. - This method iterates over the files in the provided zip archive and saves each - file to the specified temporary import path using the provided storage instance. + Only files present in allowed_files are extracted. Any file not listed + raises an error, ensuring only trusted content is written to storage. :param tmp_import_path: The temporary directory where the files will be extracted. :param zip_file: The ZipFile instance containing the files to be extracted. :param storage: The storage instance used to save the extracted files. + :param allowed_files: List of filenames permitted to be extracted. Files + not in this list cause the import to fail. :param progress_builder: A progress builder that allows for publishing progress. """ @@ -961,10 +999,24 @@ def extract_files_from_zip( ) for file_info in file_list: - extracted_file_path = join(tmp_import_path, file_info.filename) + if file_info.is_dir(): + progress.increment() + continue + + if file_info.filename not in allowed_files: + raise ImportExportResourceInvalidFile( + f"Archive contains unexpected file not listed in " + f"manifest: {file_info.filename}" + ) + + extracted_file_path = self._validate_safe_path( + tmp_import_path, file_info.filename + ) + with zip_file.open(file_info) as extracted_file: file_content = extracted_file.read() storage.save(extracted_file_path, ContentFile(file_content)) + progress.increment() def import_workspace_applications( @@ -1036,16 +1088,37 @@ def import_workspace_applications( self.mark_resource_invalid(resource) raise - self.extract_files_from_zip( - import_tmp_path, - zip_file, - storage, - progress.create_child_builder(represents_progress=10), - ) + try: + self.extract_files_from_zip( + import_tmp_path, + zip_file, + storage, + allowed_files=self._build_allowed_files(manifest_data), + progress_builder=progress.create_child_builder( + represents_progress=10 + ), + ) + except SuspiciousOperation: + self.clean_storage(import_tmp_path, storage) + self.mark_resource_invalid(resource) + raise ImportExportResourceInvalidFile( + "The import file contains invalid file paths." + ) + except Exception: + self.clean_storage(import_tmp_path, storage) + self.mark_resource_invalid(resource) + raise try: self.validate_checksums(manifest_data, import_tmp_path, storage) + except SuspiciousOperation: + self.clean_storage(import_tmp_path, storage) + self.mark_resource_invalid(resource) + raise ImportExportResourceInvalidFile( + "The import file contains invalid file paths." + ) except Exception as e: # noqa + self.clean_storage(import_tmp_path, storage) self.mark_resource_invalid(resource) raise diff --git a/backend/src/baserow/core/import_export/schema/schema_v1.0.0.json b/backend/src/baserow/core/import_export/schema/schema_v1.0.0.json index 3351e50b69..bc8990e98a 100644 --- a/backend/src/baserow/core/import_export/schema/schema_v1.0.0.json +++ b/backend/src/baserow/core/import_export/schema/schema_v1.0.0.json @@ -161,16 +161,16 @@ } } } - } - }, - "checksums": { - "type": "object", - "patternProperties": { - ".*": { - "type": "string" - } }, - "additionalProperties": false + "checksums": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + }, + "additionalProperties": false + } }, - "required": ["version", "configuration", "applications", "checksums"] + "required": ["version", "configuration", "applications", "checksums"] } diff --git a/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py b/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py index 6b1b83eb3d..0216804fda 100644 --- a/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py +++ b/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py @@ -192,6 +192,94 @@ def test_get_set_export_serialized_value_multiple_collaborators_field(data_fixtu assert imported_row_3_field[0].id == user.id +@pytest.mark.django_db(transaction=True) +@pytest.mark.field_multiple_collaborators +def test_export_import_multiple_collaborators_default_value_across_workspaces( + data_fixture, +): + user = data_fixture.create_user(email="shared@baserow.io", first_name="Shared") + user_2 = data_fixture.create_user( + email="only_source@baserow.io", first_name="OnlySource" + ) + + source_workspace = data_fixture.create_workspace(user=user) + data_fixture.create_user_workspace(workspace=source_workspace, user=user_2) + + target_workspace = data_fixture.create_workspace(user=user) + # user_2 is intentionally NOT added to target_workspace. + + database = data_fixture.create_database_application(workspace=source_workspace) + table = data_fixture.create_database_table(database=database) + collab_field = data_fixture.create_multiple_collaborators_field( + user=user, table=table, name="Collaborators" + ) + view = data_fixture.create_grid_view(user=user, table=table) + + # Create a row with collaborators so the field is exercised during export. + RowHandler().create_row( + user=user, + table=table, + values={ + f"field_{collab_field.id}": [{"id": user.id}, {"id": user_2.id}], + }, + ) + + # Set a default value with both collaborators. + from django.db import transaction + + with transaction.atomic(): + ViewHandler().update_view_default_values( + user=user, + view=view, + items=[ + { + "field": collab_field.id, + "enabled": True, + "value": [{"id": user.id}, {"id": user_2.id}], + } + ], + ) + + # Export the entire workspace. + config = ImportExportConfig(include_permission_data=False) + core_handler = CoreHandler() + exported = core_handler.export_workspace_applications( + source_workspace, BytesIO(), config + ) + + # Import into the target workspace. + imported_apps, id_mapping = core_handler.import_applications_to_workspace( + target_workspace, exported, BytesIO(), config, None + ) + + imported_database = imported_apps[0] + imported_table = imported_database.table_set.first() + imported_field = imported_table.field_set.first().specific + imported_view = imported_table.view_set.first() + + # Verify the row-level data: only user (shared) should be present because + # user_2 is not a member of the target workspace. + imported_model = imported_table.get_model() + imported_row = imported_model.objects.first() + collaborators = list( + getattr(imported_row, f"field_{imported_field.id}").order_by("id").all() + ) + assert len(collaborators) == 1 + assert collaborators[0].id == user.id + + # Verify the default value: only user (shared) should be present. + from baserow.contrib.database.views.models import ViewDefaultValue + + default_value = ViewDefaultValue.objects.get( + view=imported_view, field=imported_field + ) + assert default_value.enabled is True + assert default_value.field_type == "multiple_collaborators" + assert len(default_value.value) == 1 + assert default_value.value[0]["id"] == user.id + assert default_value.value[0]["name"] == user.first_name + + @pytest.mark.django_db def test_multiple_collaborators_field_type_sorting( data_fixture, django_assert_num_queries diff --git a/backend/tests/baserow/contrib/database/import_export/test_export_applications.py b/backend/tests/baserow/contrib/database/import_export/test_export_applications.py index 9272d06209..0e9c2a8fb6 100644 --- a/backend/tests/baserow/contrib/database/import_export/test_export_applications.py +++ b/backend/tests/baserow/contrib/database/import_export/test_export_applications.py @@ -258,7 +258,12 @@ def test_exported_files_checksum( with zipfile.ZipFile(file_path, "r") as zip_ref: manifest_data = handler.validate_manifest(zip_ref) - handler.extract_files_from_zip(path, zip_ref, storage) + handler.extract_files_from_zip( + path, + zip_ref, + storage, + allowed_files=handler._build_allowed_files(manifest_data), + ) checksums = manifest_data["checksums"] db_files = manifest_data["applications"]["database"]["items"][0]["files"] database_file = db_files["schema"] diff --git a/backend/tests/baserow/contrib/database/view/test_view_handler.py b/backend/tests/baserow/contrib/database/view/test_view_handler.py index b68aaa4549..ced3995518 100755 --- a/backend/tests/baserow/contrib/database/view/test_view_handler.py +++ b/backend/tests/baserow/contrib/database/view/test_view_handler.py @@ -4794,7 +4794,9 @@ def test_export_import_view_with_default_values(data_fixture): "database_field_select_options": MirrorDict(), } serialized["name"] = "imported view" - imported_view = view_type.import_serialized(table, serialized, config, id_mapping) + imported_view = view_type.import_serialized( + table, serialized, config, id_mapping, {} + ) # Verify imported default values. imported_defaults = handler.get_view_default_values(imported_view) @@ -4957,9 +4959,10 @@ def test_export_import_remaps_single_select_default_value(data_fixture): # Export the view. view_type = view_type_registry.get_by_model(view) config = ImportExportConfig(include_permission_data=True) + cache = {} prefetch_related_objects([view], "view_default_values") - serialized = view_type.export_serialized(view, config, {}) + serialized = view_type.export_serialized(view, config, cache) assert serialized["default_row_values"][str(field.id)]["value"] == option_a.id @@ -4971,7 +4974,9 @@ def test_export_import_remaps_single_select_default_value(data_fixture): "database_field_select_options": {option_a.id: new_option_id}, } serialized["name"] = "imported view" - imported_view = view_type.import_serialized(table, serialized, config, id_mapping) + imported_view = view_type.import_serialized( + table, serialized, config, id_mapping, {} + ) # The imported default value should have the remapped option ID. imported_record = ViewDefaultValue.objects.get(view=imported_view, field=field) @@ -5003,9 +5008,10 @@ def test_export_import_remaps_multiple_select_default_value(data_fixture): # Export the view. view_type = view_type_registry.get_by_model(view) config = ImportExportConfig(include_permission_data=True) + cache = {} prefetch_related_objects([view], "view_default_values") - serialized = view_type.export_serialized(view, config, {}) + serialized = view_type.export_serialized(view, config, cache) assert serialized["default_row_values"][str(field.id)]["value"] == [ option_a.id, @@ -5024,12 +5030,109 @@ def test_export_import_remaps_multiple_select_default_value(data_fixture): }, } serialized["name"] = "imported view" - imported_view = view_type.import_serialized(table, serialized, config, id_mapping) + imported_view = view_type.import_serialized( + table, serialized, config, id_mapping, {} + ) imported_record = ViewDefaultValue.objects.get(view=imported_view, field=field) assert imported_record.value == [new_a_id, new_b_id] +@pytest.mark.django_db +def test_export_import_remaps_multiple_collaborators_default_value(data_fixture): + user = data_fixture.create_user() + user_b = data_fixture.create_user() + workspace = data_fixture.create_workspace(users=[user, user_b]) + database = data_fixture.create_database_application(workspace=workspace) + table = data_fixture.create_database_table(database=database) + field = data_fixture.create_multiple_collaborators_field(table=table) + view = data_fixture.create_grid_view(user=user, table=table) + + # The multiple collaborators default value is stored as a list of dicts + # with an "id" key containing the user ID. + ViewDefaultValue.objects.create( + view=view, + field_id=field.id, + enabled=True, + value=[{"id": user.id}, {"id": user_b.id}], + field_type="multiple_collaborators", + ) + + # Export the view. + view_type = view_type_registry.get_by_model(view) + config = ImportExportConfig(include_permission_data=True) + + prefetch_related_objects([view], "view_default_values") + serialized = view_type.export_serialized(view, config, {}) + + # The exported value should contain email addresses, not user IDs. + exported_value = serialized["default_row_values"][str(field.id)]["value"] + assert set(exported_value) == {user.email, user_b.email} + + # Import the view back into the same table. + id_mapping = { + "workspace_id": workspace.id, + "database_fields": MirrorDict(), + "database_field_select_options": MirrorDict(), + } + serialized["name"] = "imported view" + imported_view = view_type.import_serialized( + table, serialized, config, id_mapping, {} + ) + + # The imported default value should have the resolved user IDs and names. + imported_record = ViewDefaultValue.objects.get(view=imported_view, field=field) + assert sorted(imported_record.value, key=lambda x: x["id"]) == sorted( + [ + {"id": user.id, "name": user.first_name}, + {"id": user_b.id, "name": user_b.first_name}, + ], + key=lambda x: x["id"], + ) + + +@pytest.mark.django_db +def test_export_import_multiple_collaborators_default_value_skips_missing_users( + data_fixture, +): + user = data_fixture.create_user() + workspace = data_fixture.create_workspace(users=[user]) + database = data_fixture.create_database_application(workspace=workspace) + table = data_fixture.create_database_table(database=database) + field = data_fixture.create_multiple_collaborators_field(table=table) + view = data_fixture.create_grid_view(user=user, table=table) + + # Simulate an exported value containing an email that does not exist in + # the target workspace. + view_type = view_type_registry.get_by_model(view) + config = ImportExportConfig(include_permission_data=True) + cache = {} + + serialized = view_type.export_serialized(view, config, cache) + serialized["default_row_values"] = { + str(field.id): { + "field_id": field.id, + "enabled": True, + "value": [user.email, "nonexistent@example.com"], + "function": None, + "field_type": "multiple_collaborators", + } + } + + id_mapping = { + "workspace_id": workspace.id, + "database_fields": MirrorDict(), + "database_field_select_options": MirrorDict(), + } + serialized["name"] = "imported view" + imported_view = view_type.import_serialized( + table, serialized, config, id_mapping, {} + ) + + imported_record = ViewDefaultValue.objects.get(view=imported_view, field=field) + assert imported_record.value == [{"id": user.id, "name": user.first_name}] + + @pytest.mark.django_db def test_update_view_default_values_action_stores_new_values(data_fixture): user = data_fixture.create_user() @@ -5098,7 +5201,15 @@ def test_export_import_default_values_for_all_field_types(data_fixture): # Single select: {"id": 1, "value": "A", "color": "blue"} → 1 if isinstance(value, dict) and "id" in value and "value" in value: value = value["id"] - # Link row / multiple select / multiple collaborators: + # Multiple collaborators: [{"id": 1, "name": "..."}, ...] → [{"id": 1}, ...] + elif ( + field_type.type == "multiple_collaborators" + and isinstance(value, list) + and value + and isinstance(value[0], dict) + ): + value = [{"id": item["id"]} for item in value] + # Link row / multiple select: # [{"id": 1, ...}, ...] → [1, 2, ...] elif isinstance(value, list) and value and isinstance(value[0], dict): if "id" in value[0]: @@ -5114,8 +5225,11 @@ def test_export_import_default_values_for_all_field_types(data_fixture): # Export the view. view_type = view_type_registry.get_by_model(view) config = ImportExportConfig(include_permission_data=True) + cache = { + "workspace_id": table.database.workspace.id, + } prefetch_related_objects([view], "view_default_values") - serialized = view_type.export_serialized(view, config, {}) + serialized = view_type.export_serialized(view, config, cache) assert "default_row_values" in serialized assert len(serialized["default_row_values"]) == len(items) @@ -5128,7 +5242,9 @@ def test_export_import_default_values_for_all_field_types(data_fixture): "database_field_select_options": MirrorDict(), } serialized["name"] = "imported view" - imported_view = view_type.import_serialized(table, serialized, config, id_mapping) + imported_view = view_type.import_serialized( + table, serialized, config, id_mapping, {} + ) # Verify imported default values. imported_defaults = { @@ -5145,10 +5261,20 @@ def test_export_import_default_values_for_all_field_types(data_fixture): for field_id, original in original_defaults.items(): imported = imported_defaults[field_id] - assert imported.value == original.value, ( - f"field_{field_id} ({original.field_type}): " - f"imported={imported.value!r} != original={original.value!r}" - ) + if original.field_type == "multiple_collaborators": + # The import enriches collaborator entries with the user's name, + # so we only compare the IDs. + imported_ids = sorted(item["id"] for item in imported.value) + original_ids = sorted(item["id"] for item in original.value) + assert imported_ids == original_ids, ( + f"field_{field_id} ({original.field_type}): " + f"imported IDs={imported_ids!r} != original IDs={original_ids!r}" + ) + else: + assert imported.value == original.value, ( + f"field_{field_id} ({original.field_type}): " + f"imported={imported.value!r} != original={original.value!r}" + ) assert imported.enabled == original.enabled assert imported.field_type == original.field_type assert imported.function == original.function diff --git a/backend/tests/baserow/contrib/database/view/test_view_types.py b/backend/tests/baserow/contrib/database/view/test_view_types.py index acb0725969..190bb9a0f3 100644 --- a/backend/tests/baserow/contrib/database/view/test_view_types.py +++ b/backend/tests/baserow/contrib/database/view/test_view_types.py @@ -69,6 +69,7 @@ def test_import_export_grid_view(data_fixture): serialized, ImportExportConfig(include_permission_data=False), id_mapping, + {}, None, None, ) @@ -222,6 +223,7 @@ def test_import_export_gallery_view(data_fixture, tmpdir): serialized, ImportExportConfig(include_permission_data=False), id_mapping, + {}, files_zip, storage, ) @@ -433,6 +435,7 @@ def test_import_export_form_view(data_fixture, tmpdir): serialized, ImportExportConfig(include_permission_data=False), id_mapping, + {}, files_zip, storage, ) @@ -727,6 +730,7 @@ def test_import_export_form_view_with_grouped_conditions(data_fixture, tmpdir): serialized, ImportExportConfig(include_permission_data=False), id_mapping, + {}, files_zip, storage, ) @@ -947,6 +951,7 @@ def test_import_export_view_ownership_type(data_fixture): serialized, ImportExportConfig(include_permission_data=False), {}, + {}, None, None, ) @@ -964,6 +969,7 @@ def test_import_export_view_ownership_type(data_fixture): serialized, ImportExportConfig(include_permission_data=False), {}, + {}, None, None, ) @@ -983,6 +989,7 @@ def test_import_export_view_ownership_type(data_fixture): serialized, ImportExportConfig(include_permission_data=False), {}, + {}, None, None, ) @@ -1015,6 +1022,7 @@ def test_import_export_view_ownership_type_created_by_backward_compatible(data_f serialized, ImportExportConfig(include_permission_data=False), {}, + {}, None, None, ) @@ -1055,6 +1063,7 @@ def test_import_export_view_ownership_type_not_in_registry(data_fixture): serialized, ImportExportConfig(include_permission_data=False), {}, + {}, None, None, ) diff --git a/backend/tests/baserow/contrib/integrations/core/test_smtp_email_service_type.py b/backend/tests/baserow/contrib/integrations/core/test_smtp_email_service_type.py index 9d521ae144..6f2f9bea2d 100644 --- a/backend/tests/baserow/contrib/integrations/core/test_smtp_email_service_type.py +++ b/backend/tests/baserow/contrib/integrations/core/test_smtp_email_service_type.py @@ -105,6 +105,57 @@ def test_send_smtp_email_basic(data_fixture): assert result.data == {"success": True} +@pytest.mark.django_db +@override_settings( + CELERY_EMAIL_BACKEND="anymail.backends.mailgun.EmailBackend", +) +def test_send_smtp_email_with_integration_ignores_global_celery_email_backend( + data_fixture, +): + smtp_integration = data_fixture.create_smtp_integration( + host="smtp.example.com", + port=587, + use_tls=True, + username="user@example.com", + password="password123", + ) + + service = data_fixture.create_core_smtp_email_service( + integration=smtp_integration, + use_instance_smtp_settings=False, + from_email="'sender@example.com'", + from_name="'Test Sender'", + to_emails="'recipient@example.com'", + subject="'Test Subject'", + body="'Hello, this is a test email!'", + body_type="plain", + ) + + service_type = service.get_type() + dispatch_context = FakeDispatchContext() + + with mock_django_email() as (mock_email, mock_connection): + result = service_type.dispatch(service, dispatch_context) + mock_connection.assert_called_once_with( + backend="django.core.mail.backends.smtp.EmailBackend", + host="smtp.example.com", + port=587, + username="user@example.com", + password="password123", + use_tls=True, + ) + mock_email.assert_called_once_with( + "Test Subject", + "Hello, this is a test email!", + "Test Sender ", + ["recipient@example.com"], + bcc=[], + cc=[], + connection=mock_connection.return_value, + ) + assert result.data == {"success": True} + + @pytest.mark.django_db @override_settings( INTEGRATION_ALLOW_SMTP_SERVICE_TO_USE_INSTANCE_SETTINGS=True, diff --git a/backend/tests/baserow/core/import_export/test_import_applications.py b/backend/tests/baserow/core/import_export/test_import_applications.py index f60fa10c3d..a65f9d6d43 100644 --- a/backend/tests/baserow/core/import_export/test_import_applications.py +++ b/backend/tests/baserow/core/import_export/test_import_applications.py @@ -3,11 +3,13 @@ from unittest.mock import call, patch from django.conf import settings +from django.core.exceptions import SuspiciousOperation import pytest from baserow.core.import_export.exceptions import ImportExportResourceInvalidFile from baserow.core.import_export.handler import ImportExportHandler +from baserow.core.storage import get_default_storage from baserow.test_utils.zip_helpers import ( add_file_to_zip, change_file_content_in_zip, @@ -191,3 +193,142 @@ def test_import_workspace_applications_calls_signals( mock_application_created.send.assert_has_calls(expected_calls) mock_application_imported.send.assert_has_calls(expected_calls) + + +@pytest.mark.import_export_workspace +def test_validate_safe_path_allows_normal_paths(): + handler = ImportExportHandler() + result = handler._validate_safe_path("/base/dir", "subdir/file.json") + assert result == "/base/dir/subdir/file.json" + + result = handler._validate_safe_path("/base/dir", "file.json") + assert result == "/base/dir/file.json" + + +@pytest.mark.import_export_workspace +def test_validate_safe_path_rejects_traversal(): + handler = ImportExportHandler() + + with pytest.raises(SuspiciousOperation, match="path traversal"): + handler._validate_safe_path("/base/dir", "../../../etc/passwd") + + with pytest.raises(SuspiciousOperation, match="path traversal"): + handler._validate_safe_path("/base/dir", "subdir/../../etc/passwd") + + with pytest.raises(SuspiciousOperation, match="path traversal"): + handler._validate_safe_path("/base/dir", "/etc/passwd") + + +@pytest.mark.import_export_workspace +@pytest.mark.django_db(transaction=True) +def test_import_rejects_zipslip_traversal(data_fixture, use_tmp_media_root, tmp_path): + user = data_fixture.create_user() + workspace = data_fixture.create_workspace() + data_fixture.create_import_export_trusted_source() + + zip_name = "zipslip_test.zip" + resource = data_fixture.create_import_export_resource( + created_by=user, original_name=zip_name, is_valid=True + ) + + new_zip_path = add_file_to_zip( + INTERESTING_DB_EXPORT_PATH, + f"{tmp_path}/{zip_name}", + "../../evil.txt", + b"malicious content", + ) + + with open(new_zip_path, "rb") as export_file: + content = export_file.read() + data_fixture.create_import_export_resource_file( + resource=resource, content=content + ) + + with pytest.raises(ImportExportResourceInvalidFile): + ImportExportHandler().import_workspace_applications( + user=user, + workspace=workspace, + resource=resource, + ) + + +@pytest.mark.import_export_workspace +@pytest.mark.django_db +def test_extract_files_rejects_files_not_in_manifest(tmp_path, use_tmp_media_root): + zip_path = f"{tmp_path}/allowlist_test.zip" + with zipfile.ZipFile(zip_path, "w") as zf: + zf.writestr("trusted.txt", "trusted content") + zf.writestr("extra.txt", "untrusted content") + + storage = get_default_storage() + extract_dir = "import_test/extract" + + with zipfile.ZipFile(zip_path, "r") as zf: + with pytest.raises(ImportExportResourceInvalidFile, match="unexpected file"): + ImportExportHandler().extract_files_from_zip( + extract_dir, zf, storage, allowed_files=["trusted.txt"] + ) + + +@pytest.mark.import_export_workspace +def test_build_allowed_files_includes_checksums_and_meta(): + manifest = {"checksums": {"data.json": "abc", "file.bin": "def"}} + result = ImportExportHandler._build_allowed_files(manifest) + assert "data.json" in result + assert "file.bin" in result + assert "manifest.json" in result + assert "manifest_signature.json" in result + assert len(result) == 4 + + +@pytest.mark.import_export_workspace +@pytest.mark.django_db +def test_validate_checksums_rejects_traversal(tmp_path): + handler = ImportExportHandler() + manifest = {"checksums": {"../../etc/passwd": "abc123"}} + + with pytest.raises(SuspiciousOperation, match="path traversal"): + handler.validate_checksums(manifest, str(tmp_path), get_default_storage()) + + +@pytest.mark.import_export_workspace +@pytest.mark.django_db(transaction=True) +def test_import_cleans_up_on_checksum_failure( + data_fixture, use_tmp_media_root, tmp_path +): + user = data_fixture.create_user() + workspace = data_fixture.create_workspace() + data_fixture.create_import_export_trusted_source() + + zip_name = "cleanup_test.zip" + resource = data_fixture.create_import_export_resource( + created_by=user, original_name=zip_name, is_valid=True + ) + + with zipfile.ZipFile(INTERESTING_DB_EXPORT_PATH, "r") as zip_file: + file_to_change = zip_file.namelist()[0] + + new_zip_path = change_file_content_in_zip( + INTERESTING_DB_EXPORT_PATH, + f"{tmp_path}/{zip_name}", + file_to_change, + b"tampered content", + ) + + with open(new_zip_path, "rb") as export_file: + content = export_file.read() + data_fixture.create_import_export_resource_file( + resource=resource, content=content + ) + + storage = get_default_storage() + import_tmp_path = ImportExportHandler().get_import_storage_path(resource.uuid.hex) + + with pytest.raises(ImportExportResourceInvalidFile): + ImportExportHandler().import_workspace_applications( + user=user, + workspace=workspace, + resource=resource, + ) + + assert not storage.exists(import_tmp_path) diff --git a/changelog/entries/unreleased/bug/fix_the_wrong_from_address_changed_by_the_email_backend.json b/changelog/entries/unreleased/bug/fix_the_wrong_from_address_changed_by_the_email_backend.json new file mode 100644 index 0000000000..4e0147b567 --- /dev/null +++ b/changelog/entries/unreleased/bug/fix_the_wrong_from_address_changed_by_the_email_backend.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fix the wrong from address changed by the email backend", + "issue_origin": "github", + "issue_number": null, + "domain": "integration", + "bullet_points": [], + "created_at": "2026-04-07" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/refactor/5111_harden_workspace_import_zip_extraction_against_malformed_arc.json b/changelog/entries/unreleased/refactor/5111_harden_workspace_import_zip_extraction_against_malformed_arc.json new file mode 100644 index 0000000000..226c1d1689 --- /dev/null +++ b/changelog/entries/unreleased/refactor/5111_harden_workspace_import_zip_extraction_against_malformed_arc.json @@ -0,0 +1,9 @@ +{ + "type": "refactor", + "message": "Harden workspace import ZIP extraction against malformed archives", + "issue_origin": "github", + "issue_number": 5111, + "domain": "core", + "bullet_points": [], + "created_at": "2026-04-02" +} \ No newline at end of file diff --git a/premium/backend/src/baserow_premium/views/view_types.py b/premium/backend/src/baserow_premium/views/view_types.py index d65841b438..845f2fedfb 100644 --- a/premium/backend/src/baserow_premium/views/view_types.py +++ b/premium/backend/src/baserow_premium/views/view_types.py @@ -139,7 +139,7 @@ def export_serialized( self, kanban: View, import_export_config: ImportExportConfig, - cache: Optional[Dict] = None, + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ): @@ -176,6 +176,7 @@ def import_serialized( serialized_values: Dict[str, Any], import_export_config: ImportExportConfig, id_mapping: Dict[str, Any], + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ) -> Optional[View]: @@ -196,7 +197,13 @@ def import_serialized( field_options = serialized_copy.pop("field_options") kanban_view = super().import_serialized( - table, serialized_copy, import_export_config, id_mapping, files_zip, storage + table, + serialized_copy, + import_export_config, + id_mapping, + cache, + files_zip, + storage, ) if kanban_view is not None: @@ -396,7 +403,7 @@ def export_serialized( self, calendar: View, import_export_config: ImportExportConfig, - cache: Optional[Dict] = None, + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ): @@ -430,6 +437,7 @@ def import_serialized( serialized_values: Dict[str, Any], import_export_config: ImportExportConfig, id_mapping: Dict[str, Any], + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ) -> View: @@ -446,7 +454,13 @@ def import_serialized( field_options = serialized_copy.pop("field_options") calendar_view = super().import_serialized( - table, serialized_copy, import_export_config, id_mapping, files_zip, storage + table, + serialized_copy, + import_export_config, + id_mapping, + cache, + files_zip, + storage, ) if calendar_view is not None: @@ -712,7 +726,7 @@ def export_serialized( self, timeline: View, import_export_config: ImportExportConfig, - cache: Optional[Dict] = None, + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ): @@ -748,6 +762,7 @@ def import_serialized( serialized_values: Dict[str, Any], import_export_config: ImportExportConfig, id_mapping: Dict[str, Any], + cache: Dict, files_zip: Optional[ZipFile] = None, storage: Optional[Storage] = None, ) -> View: @@ -767,7 +782,13 @@ def import_serialized( field_options = serialized_copy.pop("field_options") timeline_view = super().import_serialized( - table, serialized_copy, import_export_config, id_mapping, files_zip, storage + table, + serialized_copy, + import_export_config, + id_mapping, + cache, + files_zip, + storage, ) if "database_timeline_view_field_options" not in id_mapping: diff --git a/premium/backend/tests/baserow_premium_tests/views/test_calendar_view_type.py b/premium/backend/tests/baserow_premium_tests/views/test_calendar_view_type.py index 7047ba80c6..408bca6570 100644 --- a/premium/backend/tests/baserow_premium_tests/views/test_calendar_view_type.py +++ b/premium/backend/tests/baserow_premium_tests/views/test_calendar_view_type.py @@ -125,6 +125,7 @@ def test_calendar_view_import_export(premium_data_fixture, tmpdir): serialized = calendar_view_type.export_serialized( calendar_view, ImportExportConfig(include_permission_data=False), + cache={}, files_zip=files_zip, storage=storage, ) @@ -154,6 +155,7 @@ def test_calendar_view_import_export(premium_data_fixture, tmpdir): serialized, ImportExportConfig(include_permission_data=False), id_mapping, + {}, files_zip, storage, ) diff --git a/premium/backend/tests/baserow_premium_tests/views/test_kanban_view_type.py b/premium/backend/tests/baserow_premium_tests/views/test_kanban_view_type.py index 8277bdd99c..ba41dab17d 100644 --- a/premium/backend/tests/baserow_premium_tests/views/test_kanban_view_type.py +++ b/premium/backend/tests/baserow_premium_tests/views/test_kanban_view_type.py @@ -93,6 +93,7 @@ def test_import_export_kanban_view(premium_data_fixture, tmpdir): serialized = kanban_field_type.export_serialized( kanban_view, ImportExportConfig(include_permission_data=False), + cache={}, files_zip=files_zip, storage=storage, ) @@ -127,6 +128,7 @@ def test_import_export_kanban_view(premium_data_fixture, tmpdir): serialized, ImportExportConfig(include_permission_data=False), id_mapping, + {}, files_zip, storage, ) diff --git a/premium/backend/tests/baserow_premium_tests/views/test_premium_view_decorator_value_provider_types.py b/premium/backend/tests/baserow_premium_tests/views/test_premium_view_decorator_value_provider_types.py index 5a602f7ef8..62532a9d0e 100644 --- a/premium/backend/tests/baserow_premium_tests/views/test_premium_view_decorator_value_provider_types.py +++ b/premium/backend/tests/baserow_premium_tests/views/test_premium_view_decorator_value_provider_types.py @@ -117,7 +117,7 @@ def test_import_export_grid_view_w_decorator(data_fixture): grid_view_type = view_type_registry.get("grid") serialized = grid_view_type.export_serialized( - grid_view, ImportExportConfig(include_permission_data=False), None, None, None + grid_view, ImportExportConfig(include_permission_data=False), {}, None, None ) imported_grid_view = grid_view_type.import_serialized( grid_view.table, diff --git a/premium/backend/tests/baserow_premium_tests/views/test_timeline_view_type.py b/premium/backend/tests/baserow_premium_tests/views/test_timeline_view_type.py index 2e95cda6b9..1d021a1e0f 100644 --- a/premium/backend/tests/baserow_premium_tests/views/test_timeline_view_type.py +++ b/premium/backend/tests/baserow_premium_tests/views/test_timeline_view_type.py @@ -117,6 +117,7 @@ def test_timeline_view_import_export(premium_data_fixture, tmpdir): serialized = timeline_view_type.export_serialized( timeline_view, ImportExportConfig(include_permission_data=False), + cache={}, files_zip=files_zip, storage=storage, ) @@ -153,6 +154,7 @@ def test_timeline_view_import_export(premium_data_fixture, tmpdir): serialized, ImportExportConfig(include_permission_data=False), id_mapping, + {}, files_zip, storage, ) diff --git a/premium/web-frontend/modules/baserow_premium/components/row_comments/RowCommentsSidebar.vue b/premium/web-frontend/modules/baserow_premium/components/row_comments/RowCommentsSidebar.vue index 9701b437c3..4035f30361 100644 --- a/premium/web-frontend/modules/baserow_premium/components/row_comments/RowCommentsSidebar.vue +++ b/premium/web-frontend/modules/baserow_premium/components/row_comments/RowCommentsSidebar.vue @@ -220,8 +220,6 @@ export default { const tableId = this.table.id const rowId = this.row.id const viewId = this.view?.id - console.log(this.view) - console.log(viewId) // If the row is not an integer, it can mean that the row hasn't been created // in the backend yet. It's fine to not do anything then, because there are no diff --git a/premium/web-frontend/modules/baserow_premium/services/row_comments/row_comments.js b/premium/web-frontend/modules/baserow_premium/services/row_comments/row_comments.js index c1f58c8c09..1795aeb245 100644 --- a/premium/web-frontend/modules/baserow_premium/services/row_comments/row_comments.js +++ b/premium/web-frontend/modules/baserow_premium/services/row_comments/row_comments.js @@ -5,7 +5,6 @@ function buildViewIdParam(viewId) { export default (client) => { return { fetchAll(tableId, rowId, { offset = 0, limit = 50, viewId = null }) { - console.log(viewId) return client.get( `/row_comments/${tableId}/${rowId}/?offset=${offset}&limit=${limit}${buildViewIdParam(viewId)}` ) diff --git a/web-frontend/modules/core/components/files/FileUploaded.vue b/web-frontend/modules/core/components/files/FileUploaded.vue index 7bec8f364c..9aef1b0ceb 100644 --- a/web-frontend/modules/core/components/files/FileUploaded.vue +++ b/web-frontend/modules/core/components/files/FileUploaded.vue @@ -83,7 +83,7 @@ export default { return moment.utc(value).format('MMM Do YYYY [at] H:mm') }, formatSize(size) { - return formatFileSize(this.$i18n, size) + return formatFileSize(this.$t, this.$i18n.locale, size) }, }, } diff --git a/web-frontend/modules/core/components/import/SelectedFileDetails.vue b/web-frontend/modules/core/components/import/SelectedFileDetails.vue index 3e365d9274..2da0883316 100644 --- a/web-frontend/modules/core/components/import/SelectedFileDetails.vue +++ b/web-frontend/modules/core/components/import/SelectedFileDetails.vue @@ -71,7 +71,7 @@ export default { methods: { formatSize(bytes) { - return formatFileSize(this.$i18n, bytes) + return formatFileSize(this.$t, this.$i18n.locale, bytes) }, async handleRemove() { if (this.resourceId) { diff --git a/web-frontend/modules/core/utils/file.js b/web-frontend/modules/core/utils/file.js index aca0d570ba..497259e0bf 100644 --- a/web-frontend/modules/core/utils/file.js +++ b/web-frontend/modules/core/utils/file.js @@ -26,12 +26,10 @@ export function getFilesFromEvent(event) { * Converts an integer representing the amount of bytes to a human readable format. * Where for example 1024 will end up in 1KB. */ -export function formatFileSize($i18n, bytes) { - if (bytes === 0) return '0 ' + $i18n.t(`rowEditFieldFile.sizes.0`) +export function formatFileSize($t, locale, bytes) { + if (bytes === 0) return '0 ' + $t(`rowEditFieldFile.sizes.0`) const k = 1024 const i = Math.floor(Math.log(bytes) / Math.log(k)) - const float = parseFloat((bytes / k ** i).toFixed(2)).toLocaleString( - $i18n.locale - ) - return float + ' ' + $i18n.t(`rowEditFieldFile.sizes.${i}`) + const float = parseFloat((bytes / k ** i).toFixed(2)).toLocaleString(locale) + return float + ' ' + $t(`rowEditFieldFile.sizes.${i}`) } diff --git a/web-frontend/modules/database/components/view/DefaultValuesModal.vue b/web-frontend/modules/database/components/view/DefaultValuesModal.vue index ac1323c1f9..9dfc12ec7f 100644 --- a/web-frontend/modules/database/components/view/DefaultValuesModal.vue +++ b/web-frontend/modules/database/components/view/DefaultValuesModal.vue @@ -51,6 +51,9 @@ :field="field" :value="rowValues[`field_${field.id}`]" :read-only="false" + :workspace-id="database.workspace.id" + :row="rowValues" + :all-fields-in-table="allFields" @update="updateFieldValue(field, $event)" />