Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Scripts/openapi_generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,26 @@ prune_models
# pollution / collisions with hand-written SDK types. Runs AFTER prune_models
# so allowed_models above still matches the generator's original names.
rename_generated Action AttachmentActionPayload
rename_generated AppResponseFields AppSettings
rename_generated Field AttachmentFieldPayload
rename_generated FileUploadConfig UploadConfig
rename_generated ImageData GiphyImageData
rename_generated Images GiphyImages

rename_generated_type Response EmptyResponse

# 4c. Expose selected generated models as public API. The class and its stored
# properties become public; the memberwise init and CodingKeys stay internal.
publicize_model() {
local file="$OUTPUT_DIR_CHAT/models/$1.swift"
sed -i '' -E \
-e 's/^final class /public final class /' \
-e 's/^ let / public let /' \
"$file"
}
publicize_model AppSettings
publicize_model UploadConfig

# 5. Format.
swiftformat --config "$REPO_ROOT/.swiftformat" "$OUTPUT_DIR_CHAT"

Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ public class ChatClient: @unchecked Sendable {
apiClient.request(endpoint: .getApp()) { [weak self] result in
switch result {
case let .success(payload):
let appSettings = payload.asModel()
let appSettings = payload.app
self?.appSettings = appSettings
try? self?.backgroundWorker(of: AttachmentQueueUploader.self)
.setAppSettings(appSettings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

import Foundation

final class AppResponseFields: Sendable, Codable, JSONEncodable {
let asyncUrlEnrichEnabled: Bool
let autoTranslationEnabled: Bool
let fileUploadConfig: FileUploadConfig
let id: Int
let imageUploadConfig: FileUploadConfig
let name: String
let placement: String
public final class AppSettings: Sendable, Codable, JSONEncodable {
public let asyncUrlEnrichEnabled: Bool
public let autoTranslationEnabled: Bool
public let fileUploadConfig: UploadConfig
public let id: Int
public let imageUploadConfig: UploadConfig
public let name: String
public let placement: String

init(asyncUrlEnrichEnabled: Bool, autoTranslationEnabled: Bool, fileUploadConfig: FileUploadConfig, id: Int, imageUploadConfig: FileUploadConfig, name: String, placement: String) {
init(asyncUrlEnrichEnabled: Bool, autoTranslationEnabled: Bool, fileUploadConfig: UploadConfig, id: Int, imageUploadConfig: UploadConfig, name: String, placement: String) {
self.asyncUrlEnrichEnabled = asyncUrlEnrichEnabled
self.autoTranslationEnabled = autoTranslationEnabled
self.fileUploadConfig = fileUploadConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import Foundation

final class GetApplicationResponse: Sendable, Codable, JSONEncodable {
let app: AppResponseFields
let app: AppSettings
/// Duration of the request in milliseconds
let duration: String

init(app: AppResponseFields, duration: String) {
init(app: AppSettings, duration: String) {
self.app = app
self.duration = duration
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

import Foundation

final class FileUploadConfig: Sendable, Codable, JSONEncodable {
let allowedFileExtensions: [String]
let allowedMimeTypes: [String]
let blockedFileExtensions: [String]
let blockedMimeTypes: [String]
let sizeLimit: Int
public final class UploadConfig: Sendable, Codable, JSONEncodable {
public let allowedFileExtensions: [String]
public let allowedMimeTypes: [String]
public let blockedFileExtensions: [String]
public let blockedMimeTypes: [String]
public let sizeLimit: Int

init(allowedFileExtensions: [String], allowedMimeTypes: [String], blockedFileExtensions: [String], blockedMimeTypes: [String], sizeLimit: Int) {
self.allowedFileExtensions = allowedFileExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,83 +5,41 @@
import CoreServices
import Foundation

/// A type representing the app settings.
public struct AppSettings: Sendable {
/// The name of the app.
public let name: String
/// The the file uploading configuration.
public let fileUploadConfig: UploadConfig
/// The the image uploading configuration.
public let imageUploadConfig: UploadConfig
/// A boolean value determining if auto translation is enabled.
public let autoTranslationEnabled: Bool
/// A boolean value determining if async url enrichment is enabled.
public let asyncUrlEnrichEnabled: Bool

public struct UploadConfig: Sendable {
/// The allowed file extensions.
public let allowedFileExtensions: [String]
/// The blocked file extensions.
public let blockedFileExtensions: [String]
/// The allowed mime types.
public let allowedMimeTypes: [String]
/// The blocked mime types.
public let blockedMimeTypes: [String]
/// The file size limit allowed in Bytes.
/// This value is configurable from Stream's Dashboard App Settings.
public let sizeLimitInBytes: Int64?
}
}

// MARK: - Response -> Model

extension GetApplicationResponse {
func asModel() -> AppSettings {
.init(
name: app.name,
fileUploadConfig: app.fileUploadConfig.asModel(),
imageUploadConfig: app.imageUploadConfig.asModel(),
autoTranslationEnabled: app.autoTranslationEnabled,
asyncUrlEnrichEnabled: app.asyncUrlEnrichEnabled
)
}
extension AppSettings {
/// The upload configuration.
public typealias UploadConfig = StreamChat.UploadConfig
}

extension FileUploadConfig {
func asModel() -> AppSettings.UploadConfig {
.init(
allowedFileExtensions: allowedFileExtensions,
blockedFileExtensions: blockedFileExtensions,
allowedMimeTypes: allowedMimeTypes,
blockedMimeTypes: blockedMimeTypes,
sizeLimitInBytes: Int64(sizeLimit)
)
}
extension AppSettings.UploadConfig {
/// The file size limit allowed in Bytes.
/// This value is configurable from Stream's Dashboard App Settings.
@available(*, deprecated, renamed: "sizeLimit")
public var sizeLimitInBytes: Int64? { Int64(sizeLimit) }
}
Comment on lines +13 to 18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

sizeLimitInBytes can no longer represent "no limit configured".

sizeLimit (generated) is a non-optional Int, so Int64(sizeLimit) never returns nil. Given the codebase's own convention elsewhere (ComposerVC.maxAttachmentSize treats sizeLimit <= 0 as "use default"), the deprecated sizeLimitInBytes previously likely returned nil to signal "no limit configured." Any remaining caller (internal or third-party via this public deprecated API) branching on nil vs a value will now always take the "configured" branch, silently changing behavior for the 0/unset case.

🐛 Proposed fix to preserve nil semantics
     `@available`(*, deprecated, renamed: "sizeLimit")
-    public var sizeLimitInBytes: Int64? { Int64(sizeLimit) }
+    public var sizeLimitInBytes: Int64? { sizeLimit > 0 ? Int64(sizeLimit) : nil }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
extension AppSettings.UploadConfig {
/// The file size limit allowed in Bytes.
/// This value is configurable from Stream's Dashboard App Settings.
@available(*, deprecated, renamed: "sizeLimit")
public var sizeLimitInBytes: Int64? { Int64(sizeLimit) }
}
extension AppSettings.UploadConfig {
/// The file size limit allowed in Bytes.
/// This value is configurable from Stream's Dashboard App Settings.
`@available`(*, deprecated, renamed: "sizeLimit")
public var sizeLimitInBytes: Int64? { sizeLimit > 0 ? Int64(sizeLimit) : nil }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamChat/Models/AppSettings`+Extensions.swift around lines 13 - 18,
The deprecated AppSettings.UploadConfig.sizeLimitInBytes property currently
always returns a value because sizeLimit is non-optional, which breaks the
previous “no limit configured” nil semantics. Update sizeLimitInBytes to
preserve the old behavior by returning nil when sizeLimit is unset or
non-positive (matching the existing sizeLimit <= 0 convention used elsewhere,
such as ComposerVC.maxAttachmentSize), and only return Int64(sizeLimit) for real
configured limits. Keep the change localized to
AppSettings.UploadConfig.sizeLimitInBytes so existing callers continue to branch
correctly on nil versus a valid limit.


// MARK: - Validation

extension AppSettings.UploadConfig {
// MARK: - UTI Validation

/// Returns an array of allowed UTI identifiers based on allowed mime types and file extensions.
public var allowedUTITypes: [String] {
allowedMimeTypes.compactMap { $0.utiType(mime: true) } +
allowedFileExtensions.compactMap { $0.utiType(mime: false) }
}

/// Returns an array of blocked UTI identifiers based on allowed mime types and file extensions.
public var blockedUTITypes: [String] {
blockedMimeTypes.compactMap { $0.utiType(mime: true) } +
blockedFileExtensions.compactMap { $0.utiType(mime: false) }
}

// MARK: - URL Validation

func isAllowed(localURL: URL) -> Bool {
guard !localURL.pathExtension.isEmpty else { return true }

Comment on lines 39 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file outline =="
ast-grep outline Sources/StreamChat/Models/AppSettings+Extensions.swift --view expanded || true

echo
echo "== target file excerpt =="
nl -ba Sources/StreamChat/Models/AppSettings+Extensions.swift | sed -n '1,220p'

echo
echo "== usages of isAllowed(localURL:) =="
rg -n "isAllowed\\(localURL:" -S Sources || true

echo
echo "== references to AttachmentFileType(ext: or mimeType checks =="
rg -n "AttachmentFileType\\(ext:|mimeType|allowedFileExtensions|allowedMimeTypes|upload validation|isAllowed\\(" Sources -S || true

Repository: GetStream/stream-chat-swift

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file excerpt =="
sed -n '1,180p' Sources/StreamChat/Models/AppSettings+Extensions.swift | cat -n

echo
echo "== usages of isAllowed(localURL:) =="
rg -n "isAllowed\\(localURL:" Sources -S || true

echo
echo "== nearby upload validation references =="
rg -n "allowedFileExtensions|allowedMimeTypes|isAllowed\\(|AttachmentFileType\\(|mimeType" Sources/StreamChat -S || true

Repository: GetStream/stream-chat-swift

Length of output: 11946


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AttachmentQueueUploader excerpt around validation =="
sed -n '150,210p' Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift | cat -n

echo
echo "== AttachmentFileType initializer and mimeType =="
sed -n '260,330p' Sources/StreamChat/Models/Attachments/AttachmentTypes.swift | cat -n

echo
echo "== AttachmentFileType ext-based usage =="
rg -n "init\\(ext:|AttachmentFileType\\(ext:" Sources/StreamChat/Models/Attachments/AttachmentTypes.swift Sources/StreamChat -S || true

Repository: GetStream/stream-chat-swift

Length of output: 6781


Extensionless uploads bypass configured upload validation Sources/StreamChat/Models/AppSettings+Extensions.swift:39

guard !localURL.pathExtension.isEmpty else { return true } skips both the extension and MIME-type allow/block checks, so a local file with no extension can bypass dashboard upload restrictions. If extensionless files should still honor those rules, remove this early return or gate it behind the config checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamChat/Models/AppSettings`+Extensions.swift around lines 39 - 41,
The early return in AppSettings+Extensions.isAllowed(localURL:) lets
extensionless files bypass the configured upload validation entirely. Update
this method so a missing pathExtension does not immediately return true;
instead, let the existing extension and MIME-type allow/block logic run for
extensionless URLs, or only skip validation if the settings explicitly permit
it. Use the isAllowed(localURL:) flow and its related upload restriction checks
to keep dashboard-configured rules enforced.

if !allowedFileExtensions.isEmpty || !blockedFileExtensions.isEmpty {

Check warning on line 42 in Sources/StreamChat/Models/AppSettings+Extensions.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this if statement with the nested one.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swift&issues=AZ83hNEnQiiiS6kKxeSF&open=AZ83hNEnQiiiS6kKxeSF&pullRequest=4159
if !isAllowed(pathExtension: localURL.pathExtension.lowercased()) {
return false
}
Expand All @@ -94,7 +52,7 @@
}
return true
}

private func isAllowed(pathExtension: String) -> Bool {
let isBlocked = blockedFileExtensions.contains { blocked in
blocked.drop(while: { $0 == Character(".") }).caseInsensitiveCompare(pathExtension) == .orderedSame
Expand All @@ -105,7 +63,7 @@
allowed.drop(while: { $0 == Character(".") }).caseInsensitiveCompare(pathExtension) == .orderedSame
}
}

private func isAllowed(mimeType: String) -> Bool {
let isBlocked = blockedMimeTypes.contains { blocked in
blocked.caseInsensitiveCompare(mimeType) == .orderedSame
Expand Down
10 changes: 5 additions & 5 deletions Sources/StreamChatUI/Composer/ComposerVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1505,19 +1505,19 @@ open class ComposerVC: _ViewController,
return Components.default.maxAttachmentSize
}

let maxAttachmentSize: Int64?
let maxAttachmentSize: Int
switch attachmentType {
case .image:
maxAttachmentSize = client.appSettings?.imageUploadConfig.sizeLimitInBytes
maxAttachmentSize = client.appSettings?.imageUploadConfig.sizeLimit ?? 0
default:
maxAttachmentSize = client.appSettings?.fileUploadConfig.sizeLimitInBytes
maxAttachmentSize = client.appSettings?.fileUploadConfig.sizeLimit ?? 0
}

guard let maxSize = maxAttachmentSize, maxSize > 0 else {
guard maxAttachmentSize > 0 else {
return Components.default.maxAttachmentSize
}

return maxSize
return Int64(maxAttachmentSize)
}

/// Shows an alert for the error thrown when adding attachment to a composer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,11 +425,13 @@ extension AppSettings {
asyncUrlEnrichEnabled: Bool = false
) -> AppSettings {
.init(
name: name,
asyncUrlEnrichEnabled: asyncUrlEnrichEnabled,
autoTranslationEnabled: autoTranslationEnabled,
fileUploadConfig: fileUploadConfig ?? .mock(),
id: 1,
imageUploadConfig: imageUploadConfig ?? .mock(),
autoTranslationEnabled: autoTranslationEnabled,
asyncUrlEnrichEnabled: asyncUrlEnrichEnabled
name: name,
placement: ""
)
}
}
Expand All @@ -444,10 +446,10 @@ extension AppSettings.UploadConfig {
) -> AppSettings.UploadConfig {
.init(
allowedFileExtensions: allowedFileExtensions,
blockedFileExtensions: blockedFileExtensions,
allowedMimeTypes: allowedMimeTypes,
blockedFileExtensions: blockedFileExtensions,
blockedMimeTypes: blockedMimeTypes,
sizeLimitInBytes: sizeLimitInBytes
sizeLimit: sizeLimitInBytes.map(Int.init) ?? 0
)
}
}
Loading